aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2021-09-04 14:35:05 +0100
committerGravatar GitHub <[email protected]>2021-09-04 14:35:05 +0100
commitdc26ff93014268db85cc0bcd15cdd857ca33fe09 (patch)
treeb59ac4d37cbc11009faa6865dce85a3fc6aa0b41
parentAdd concurrency rules to all GitHub workflows (diff)
parentMerge pull request #1808 from python-discord/jb3/update-manifest-path (diff)
Merge branch 'main' into Add-concurrency-to-all-workflows
-rw-r--r--.github/workflows/deploy.yml2
-rw-r--r--.github/workflows/lint-test.yml18
-rw-r--r--bot/constants.py2
-rw-r--r--bot/exts/info/doc/_cog.py15
-rw-r--r--bot/exts/moderation/dm_relay.py6
-rw-r--r--bot/exts/moderation/infraction/management.py25
-rw-r--r--bot/exts/recruitment/talentpool/_cog.py164
-rw-r--r--bot/exts/recruitment/talentpool/_review.py28
-rw-r--r--bot/resources/tags/botvar.md (renamed from bot/resources/tags/bot_var.md)2
-rw-r--r--config-default.yml3
-rw-r--r--poetry.lock201
-rw-r--r--pyproject.toml1
12 files changed, 284 insertions, 183 deletions
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 0f030ebd7..88abe6fb6 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -42,6 +42,6 @@ jobs:
uses: Azure/k8s-deploy@v1
with:
manifests: |
- bot/deployment.yaml
+ namespaces/default/bot/deployment.yaml
images: 'ghcr.io/python-discord/bot:${{ steps.sha_tag.outputs.tag }}'
kubectl-version: 'latest'
diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index c4f87d522..619544e1a 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -14,6 +14,17 @@ jobs:
lint-test:
runs-on: ubuntu-latest
env:
+ # List of licenses that are compatible with the MIT License and
+ # can be used in our project
+ ALLOWED_LICENSE: Apache Software License;
+ BSD License;
+ GNU Library or Lesser General Public License (LGPL);
+ ISC License (ISCL);
+ MIT License;
+ Mozilla Public License 2.0 (MPL 2.0);
+ Public Domain;
+ Python Software Foundation License
+
# Dummy values for required bot environment variables
BOT_API_KEY: foo
BOT_SENTRY_DSN: blah
@@ -70,6 +81,13 @@ jobs:
pip install poetry
poetry install
+ # Check all the dependencies are compatible with the MIT license.
+ # If you added a new dependencies that is being rejected,
+ # please make sure it is compatible with the license for this project,
+ # and add it to the ALLOWED_LICENSE variable
+ - name: Check Dependencies License
+ run: pip-licenses --allow-only="$ALLOWED_LICENSE"
+
# 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.
diff --git a/bot/constants.py b/bot/constants.py
index 4e99df7f3..f99913b17 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -472,7 +472,6 @@ class Channels(metaclass=YAMLGetter):
voice_chat_1: int
big_brother_logs: int
- talent_pool: int
class Webhooks(metaclass=YAMLGetter):
@@ -483,7 +482,6 @@ class Webhooks(metaclass=YAMLGetter):
dev_log: int
duck_pond: int
incidents_archive: int
- talent_pool: int
class Roles(metaclass=YAMLGetter):
diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py
index fb9b2584a..a2119a53d 100644
--- a/bot/exts/info/doc/_cog.py
+++ b/bot/exts/info/doc/_cog.py
@@ -150,6 +150,8 @@ class DocCog(commands.Cog):
self.update_or_reschedule_inventory(api_package_name, base_url, inventory_url),
)
else:
+ if not base_url:
+ base_url = self.base_url_from_inventory_url(inventory_url)
self.update_single(api_package_name, base_url, package)
def ensure_unique_symbol_name(self, package_name: str, group_name: str, symbol_name: str) -> str:
@@ -352,6 +354,11 @@ class DocCog(commands.Cog):
msg = await ctx.send(embed=doc_embed)
await wait_for_deletion(msg, (ctx.author.id,))
+ @staticmethod
+ def base_url_from_inventory_url(inventory_url: str) -> str:
+ """Get a base url from the url to an objects inventory by removing the last path segment."""
+ return inventory_url.removesuffix("/").rsplit("/", maxsplit=1)[0] + "/"
+
@docs_group.command(name="setdoc", aliases=("s",))
@commands.has_any_role(*MODERATION_ROLES)
@lock(NAMESPACE, COMMAND_LOCK_SINGLETON, raise_error=True)
@@ -359,21 +366,21 @@ class DocCog(commands.Cog):
self,
ctx: commands.Context,
package_name: PackageName,
- base_url: ValidURL,
inventory: Inventory,
+ base_url: ValidURL = "",
) -> None:
"""
Adds a new documentation metadata object to the site's database.
The database will update the object, should an existing item with the specified `package_name` already exist.
+ If the base url is not specified, a default created by removing the last segment of the inventory url is used.
Example:
!docs setdoc \
python \
- https://docs.python.org/3/ \
https://docs.python.org/3/objects.inv
"""
- if not base_url.endswith("/"):
+ if base_url and not base_url.endswith("/"):
raise commands.BadArgument("The base url must end with a slash.")
inventory_url, inventory_dict = inventory
body = {
@@ -388,6 +395,8 @@ class DocCog(commands.Cog):
+ "\n".join(f"{key}: {value}" for key, value in body.items())
)
+ if not base_url:
+ base_url = self.base_url_from_inventory_url(inventory_url)
self.update_single(package_name, base_url, inventory_dict)
await ctx.send(f"Added the package `{package_name}` to the database and updated the inventories.")
diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py
index 1d2206e27..0051db82f 100644
--- a/bot/exts/moderation/dm_relay.py
+++ b/bot/exts/moderation/dm_relay.py
@@ -5,6 +5,7 @@ from discord.ext.commands import Cog, Context, command, has_any_role
from bot.bot import Bot
from bot.constants import Emojis, MODERATION_ROLES
+from bot.utils.channel import is_mod_channel
from bot.utils.services import send_to_paste_service
log = logging.getLogger(__name__)
@@ -63,8 +64,9 @@ class DMRelay(Cog):
await ctx.send(paste_link)
async def cog_check(self, ctx: Context) -> bool:
- """Only allow moderators to invoke the commands in this cog."""
- return await has_any_role(*MODERATION_ROLES).predicate(ctx)
+ """Only allow moderators to invoke the commands in this cog in mod channels."""
+ return (await has_any_role(*MODERATION_ROLES).predicate(ctx)
+ and is_mod_channel(ctx.channel))
def setup(bot: Bot) -> None:
diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py
index 7f27896d7..223a124d8 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -11,6 +11,7 @@ from discord.ext.commands import Context
from discord.utils import escape_markdown
from bot import constants
+from bot.api import ResponseCodeError
from bot.bot import Bot
from bot.converters import Expiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser, allowed_strings
from bot.exts.moderation.infraction.infractions import Infractions
@@ -44,9 +45,25 @@ class ModManagement(commands.Cog):
# region: Edit infraction commands
@commands.group(name='infraction', aliases=('infr', 'infractions', 'inf', 'i'), invoke_without_command=True)
- async def infraction_group(self, ctx: Context) -> None:
- """Infraction manipulation commands."""
- await ctx.send_help(ctx.command)
+ async def infraction_group(self, ctx: Context, infr_id: int = None) -> None:
+ """Infraction manipulation commands. If `infr_id` is passed then this command fetches that infraction."""
+ if infr_id is None:
+ await ctx.send_help(ctx.command)
+ return
+
+ try:
+ infraction_list = [await self.bot.api_client.get(f"bot/infractions/{infr_id}/expanded")]
+ except ResponseCodeError as e:
+ if e.status == 404:
+ await ctx.send(f":x: No infraction with ID `{infr_id}` could be found.")
+ return
+ raise e
+
+ embed = discord.Embed(
+ title=f"Infraction #{infr_id}",
+ colour=discord.Colour.orange()
+ )
+ await self.send_infraction_list(ctx, embed, infraction_list)
@infraction_group.command(name="append", aliases=("amend", "add", "a"))
async def infraction_append(
@@ -210,7 +227,7 @@ class ModManagement(commands.Cog):
else:
await self.search_user(ctx, query)
- @infraction_search_group.command(name="user", aliases=("member", "id"))
+ @infraction_search_group.command(name="user", aliases=("member", "userid"))
async def search_user(self, ctx: Context, user: t.Union[MemberOrUser, discord.Object]) -> None:
"""Search for infractions by member."""
infraction_list = await self.bot.api_client.get(
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py
index c297f70c2..a317c6645 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -1,6 +1,6 @@
import logging
import textwrap
-from collections import ChainMap
+from collections import ChainMap, defaultdict
from io import StringIO
from typing import Union
@@ -11,12 +11,12 @@ from discord.ext.commands import Cog, Context, group, has_any_role
from bot.api import ResponseCodeError
from bot.bot import Bot
-from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES, Webhooks
+from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES
from bot.converters import MemberOrUser
-from bot.exts.moderation.watchchannels._watchchannel import WatchChannel
from bot.exts.recruitment.talentpool._review import Reviewer
from bot.pagination import LinePaginator
from bot.utils import time
+from bot.utils.time import get_time_delta
AUTOREVIEW_ENABLED_KEY = "autoreview_enabled"
REASON_MAX_CHARS = 1000
@@ -24,25 +24,17 @@ REASON_MAX_CHARS = 1000
log = logging.getLogger(__name__)
-class TalentPool(WatchChannel, Cog, name="Talentpool"):
- """Relays messages of helper candidates to a watch channel to observe them."""
+class TalentPool(Cog, name="Talentpool"):
+ """Used to nominate potential helper candidates."""
# RedisCache[str, bool]
# Can contain a single key, "autoreview_enabled", with the value a bool indicating if autoreview is enabled.
talentpool_settings = RedisCache()
def __init__(self, bot: Bot) -> None:
- super().__init__(
- bot,
- destination=Channels.talent_pool,
- webhook_id=Webhooks.talent_pool,
- api_endpoint='bot/nominations',
- api_default_params={'active': 'true', 'ordering': '-inserted_at'},
- logger=log,
- disable_header=True,
- )
-
+ self.bot = bot
self.reviewer = Reviewer(self.__class__.__name__, bot, self)
+ self.api_default_params = {'active': 'true', 'ordering': '-inserted_at'}
self.bot.loop.create_task(self.schedule_autoreviews())
async def schedule_autoreviews(self) -> None:
@@ -50,12 +42,31 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
if await self.autoreview_enabled():
await self.reviewer.reschedule_reviews()
else:
- self.log.trace("Not scheduling reviews as autoreview is disabled.")
+ log.trace("Not scheduling reviews as autoreview is disabled.")
async def autoreview_enabled(self) -> bool:
"""Return whether automatic posting of nomination reviews is enabled."""
return await self.talentpool_settings.get(AUTOREVIEW_ENABLED_KEY, True)
+ async def refresh_cache(self) -> bool:
+ """Updates TalentPool users cache."""
+ try:
+ data = await self.bot.api_client.get(
+ 'bot/nominations',
+ params=self.api_default_params
+ )
+ except ResponseCodeError as err:
+ log.exception("Failed to fetch the currently nominated users from the API", exc_info=err)
+ return False
+
+ self.cache = defaultdict(dict)
+
+ for entry in data:
+ user_id = entry.pop('user')
+ self.cache[user_id] = entry
+
+ return True
+
@group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def nomination_group(self, ctx: Context) -> None:
@@ -106,25 +117,29 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
else:
await ctx.send("Autoreview is currently disabled")
- @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",))
+ @nomination_group.command(
+ name="nominees",
+ aliases=("nominated", "all", "list", "watched"),
+ root_aliases=("nominees",)
+ )
@has_any_role(*MODERATION_ROLES)
- async def watched_command(
+ async def list_command(
self,
ctx: Context,
oldest_first: bool = False,
update_cache: bool = True
) -> None:
"""
- Shows the users that are currently being monitored in the talent pool.
+ Shows the users that are currently in the talent pool.
The optional kwarg `oldest_first` can be used to order the list by oldest nomination.
The optional kwarg `update_cache` can be used to update the user
cache using the API before listing the users.
"""
- await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache)
+ await self.list_nominated_users(ctx, oldest_first=oldest_first, update_cache=update_cache)
- async def list_watched_users(
+ async def list_nominated_users(
self,
ctx: Context,
oldest_first: bool = False,
@@ -141,16 +156,27 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
The optional kwarg `update_cache` specifies whether the cache should
be refreshed by polling the API.
"""
- # TODO Once the watch channel is removed, this can be done in a smarter way, without splitting and overriding
- # the list_watched_users function.
- watched_data = await self.prepare_watched_users_data(ctx, oldest_first, update_cache)
+ successful_update = False
+ if update_cache:
+ if not (successful_update := await self.refresh_cache()):
+ await ctx.send(":warning: Unable to update cache. Data may be inaccurate.")
- if update_cache and not watched_data["updated"]:
- await ctx.send(f":x: Failed to update {self.__class__.__name__} user cache, serving from cache")
+ nominations = self.cache.items()
+ if oldest_first:
+ nominations = reversed(nominations)
lines = []
- for user_id, line in watched_data["info"].items():
- if self.watched_users[user_id]['reviewed']:
+
+ for user_id, user_data in nominations:
+ member = ctx.guild.get_member(user_id)
+ line = f"• `{user_id}`"
+ if member:
+ line += f" ({member.name}#{member.discriminator})"
+ inserted_at = user_data['inserted_at']
+ line += f", added {get_time_delta(inserted_at)}"
+ if not member: # Cross off users who left the server.
+ line = f"~~{line}~~"
+ if user_data['reviewed']:
line += " *(reviewed)*"
elif user_id in self.reviewer:
line += " *(scheduled)*"
@@ -160,7 +186,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
lines = ("There's nothing here yet.",)
embed = Embed(
- title=watched_data["title"],
+ title=f"Talent Pool active nominations ({'updated' if update_cache and successful_update else 'cached'})",
color=Color.blue()
)
await LinePaginator.paginate(lines, ctx, embed, empty=False)
@@ -169,26 +195,30 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@has_any_role(*MODERATION_ROLES)
async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None:
"""
- Shows talent pool monitored users ordered by oldest nomination.
+ Shows talent pool users ordered by oldest nomination.
The optional kwarg `update_cache` can be used to update the user
cache using the API before listing the users.
"""
- await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache)
+ await ctx.invoke(self.list_command, oldest_first=True, update_cache=update_cache)
- @nomination_group.command(name='forcewatch', aliases=('fw', 'forceadd', 'fa'), root_aliases=("forcenominate",))
+ @nomination_group.command(
+ name="forcenominate",
+ aliases=("fw", "forceadd", "fa", "fn", "forcewatch"),
+ root_aliases=("forcenominate",)
+ )
@has_any_role(*MODERATION_ROLES)
- async def force_watch_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:
+ async def force_nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:
"""
Adds the given `user` to the talent pool, from any channel.
A `reason` for adding the user to the talent pool is optional.
"""
- await self._watch_user(ctx, user, reason)
+ await self._nominate_user(ctx, user, reason)
- @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",))
+ @nomination_group.command(name='nominate', aliases=("w", "add", "a", "watch"), root_aliases=("nominate",))
@has_any_role(*STAFF_ROLES)
- async def watch_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:
+ async def nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:
"""
Adds the given `user` to the talent pool.
@@ -199,26 +229,26 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
if any(role.id in MODERATION_ROLES for role in ctx.author.roles):
await ctx.send(
f":x: Nominations should be run in the <#{Channels.nominations}> channel. "
- "Use `!tp forcewatch` to override this check."
+ "Use `!tp forcenominate` to override this check."
)
else:
await ctx.send(f":x: Nominations must be run in the <#{Channels.nominations}> channel")
return
- await self._watch_user(ctx, user, reason)
+ await self._nominate_user(ctx, user, reason)
- async def _watch_user(self, ctx: Context, user: MemberOrUser, reason: str) -> None:
+ async def _nominate_user(self, ctx: Context, user: MemberOrUser, reason: str) -> None:
"""Adds the given user to the talent pool."""
if user.bot:
- await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.")
+ await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. Only humans can be nominated.")
return
if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles):
await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:")
return
- if not await self.fetch_user_cache():
- await ctx.send(f":x: Failed to update the user cache; can't add {user}")
+ if not await self.refresh_cache():
+ await ctx.send(f":x: Failed to update the cache; can't add {user}")
return
if len(reason) > REASON_MAX_CHARS:
@@ -227,7 +257,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
# Manual request with `raise_for_status` as False because we want the actual response
session = self.bot.api_client.session
- url = self.bot.api_client._url_for(self.api_endpoint)
+ url = self.bot.api_client._url_for('bot/nominations')
kwargs = {
'json': {
'actor': ctx.author.id,
@@ -249,13 +279,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
else:
resp.raise_for_status()
- self.watched_users[user.id] = response_data
+ self.cache[user.id] = response_data
if await self.autoreview_enabled() and user.id not in self.reviewer:
self.reviewer.schedule_review(user.id)
history = await self.bot.api_client.get(
- self.api_endpoint,
+ 'bot/nominations',
params={
"user__id": str(user.id),
"active": "false",
@@ -274,7 +304,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
async def history_command(self, ctx: Context, user: MemberOrUser) -> None:
"""Shows the specified user's nomination history."""
result = await self.bot.api_client.get(
- self.api_endpoint,
+ 'bot/nominations',
params={
'user__id': str(user.id),
'ordering': "-active,-inserted_at"
@@ -298,20 +328,20 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
max_size=1000
)
- @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",))
+ @nomination_group.command(name="end", aliases=("unwatch", "unnominate"), root_aliases=("unnominate",))
@has_any_role(*MODERATION_ROLES)
- async def unwatch_command(self, ctx: Context, user: MemberOrUser, *, reason: str) -> None:
+ async def end_nomination_command(self, ctx: Context, user: MemberOrUser, *, reason: str) -> None:
"""
Ends the active nomination of the specified user with the given reason.
Providing a `reason` is required.
"""
if len(reason) > REASON_MAX_CHARS:
- await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.")
+ await ctx.send(f":x: Maximum allowed characters for the end reason is {REASON_MAX_CHARS}.")
return
- if await self.unwatch(user.id, reason):
- await ctx.send(f":white_check_mark: Messages sent by {user.mention} will no longer be relayed")
+ if await self.end_nomination(user.id, reason):
+ await ctx.send(f":white_check_mark: Successfully un-nominated {user}")
else:
await ctx.send(":x: The specified user does not have an active nomination")
@@ -330,10 +360,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
return
try:
- nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}")
+ nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}")
except ResponseCodeError as e:
if e.response.status == 404:
- self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
+ log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`")
return
else:
@@ -347,13 +377,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
await ctx.send(f":x: {actor.mention} doesn't have an entry in this nomination.")
return
- self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}")
+ log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}")
await self.bot.api_client.patch(
- f"{self.api_endpoint}/{nomination_id}",
+ f"bot/nominations/{nomination_id}",
json={"actor": actor.id, "reason": reason}
)
- await self.fetch_user_cache() # Update cache
+ await self.refresh_cache() # Update cache
await ctx.send(":white_check_mark: Successfully updated nomination reason.")
@nomination_edit_group.command(name='end_reason')
@@ -365,10 +395,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
return
try:
- nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}")
+ nomination = await self.bot.api_client.get(f"bot/nominations/{nomination_id}")
except ResponseCodeError as e:
if e.response.status == 404:
- self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
+ log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}")
await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`")
return
else:
@@ -378,13 +408,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
await ctx.send(":x: Can't edit the end reason of an active nomination.")
return
- self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}")
+ log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}")
await self.bot.api_client.patch(
- f"{self.api_endpoint}/{nomination_id}",
+ f"bot/nominations/{nomination_id}",
json={"end_reason": reason}
)
- await self.fetch_user_cache() # Update cache.
+ await self.refresh_cache() # Update cache.
await ctx.send(":white_check_mark: Updated the end reason of the nomination!")
@nomination_group.command(aliases=('mr',))
@@ -419,7 +449,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@Cog.listener()
async def on_member_ban(self, guild: Guild, user: Union[MemberOrUser]) -> None:
"""Remove `user` from the talent pool after they are banned."""
- await self.unwatch(user.id, "User was banned.")
+ await self.end_nomination(user.id, "User was banned.")
@Cog.listener()
async def on_raw_reaction_add(self, payload: RawReactionActionEvent) -> None:
@@ -441,10 +471,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
log.info(f"Archiving nomination {message.id}")
await self.reviewer.archive_vote(message, emoji == Emojis.incident_actioned)
- async def unwatch(self, user_id: int, reason: str) -> bool:
+ async def end_nomination(self, user_id: int, reason: str) -> bool:
"""End the active nomination of a user with the given reason and return True on success."""
active_nomination = await self.bot.api_client.get(
- self.api_endpoint,
+ 'bot/nominations',
params=ChainMap(
{"user__id": str(user_id)},
self.api_default_params,
@@ -459,11 +489,11 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
nomination = active_nomination[0]
await self.bot.api_client.patch(
- f"{self.api_endpoint}/{nomination['id']}",
+ f"bot/nominations/{nomination['id']}",
json={'end_reason': reason, 'active': False}
)
- self._remove_user(user_id)
+ self.cache.pop(user_id)
if await self.autoreview_enabled():
self.reviewer.cancel(user_id)
@@ -512,7 +542,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
{entries_string}
End date: {end_date}
- Unwatch reason: {nomination_object["end_reason"]}
+ Unnomination reason: {nomination_object["end_reason"]}
===============
"""
)
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index 4d496a1f7..3ffbf93f3 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -58,9 +58,9 @@ class Reviewer:
log.trace("Rescheduling reviews")
await self.bot.wait_until_guild_available()
# TODO Once the watch channel is removed, this can be done in a smarter way, e.g create a sync function.
- await self._pool.fetch_user_cache()
+ await self._pool.refresh_cache()
- for user_id, user_data in self._pool.watched_users.items():
+ for user_id, user_data in self._pool.cache.items():
if not user_data["reviewed"]:
self.schedule_review(user_id)
@@ -68,7 +68,7 @@ class Reviewer:
"""Schedules a single user for review."""
log.trace(f"Scheduling review of user with ID {user_id}")
- user_data = self._pool.watched_users.get(user_id)
+ user_data = self._pool.cache.get(user_id)
inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None)
review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL)
@@ -96,18 +96,18 @@ class Reviewer:
await last_message.add_reaction(reaction)
if update_database:
- nomination = self._pool.watched_users.get(user_id)
- await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True})
+ nomination = self._pool.cache.get(user_id)
+ await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True})
async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji]]:
"""Format a generic review of a user and return it with the reviewed emoji."""
log.trace(f"Formatting the review of {user_id}")
- # Since `watched_users` is a defaultdict, we should take care
+ # Since `cache` is a defaultdict, we should take care
# not to accidentally insert the IDs of users that have no
- # active nominated by using the `watched_users.get(user_id)`
- # instead of `watched_users[user_id]`.
- nomination = self._pool.watched_users.get(user_id)
+ # active nominated by using the `cache.get(user_id)`
+ # instead of `cache[user_id]`.
+ nomination = self._pool.cache.get(user_id)
if not nomination:
log.trace(f"There doesn't appear to be an active nomination for {user_id}")
return "", None
@@ -332,7 +332,7 @@ class Reviewer:
"""
log.trace(f"Fetching the nomination history data for {member.id}'s review")
history = await self.bot.api_client.get(
- self._pool.api_endpoint,
+ "bot/nominations",
params={
"user__id": str(member.id),
"active": "false",
@@ -390,18 +390,18 @@ class Reviewer:
Returns True if the user was successfully marked as reviewed, False otherwise.
"""
log.trace(f"Updating user {user_id} as reviewed")
- await self._pool.fetch_user_cache()
- if user_id not in self._pool.watched_users:
+ await self._pool.refresh_cache()
+ if user_id not in self._pool.cache:
log.trace(f"Can't find a nominated user with id {user_id}")
await ctx.send(f":x: Can't find a currently nominated user with id `{user_id}`")
return False
- nomination = self._pool.watched_users.get(user_id)
+ nomination = self._pool.cache.get(user_id)
if nomination["reviewed"]:
await ctx.send(":x: This nomination was already reviewed, but here's a cookie :cookie:")
return False
- await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True})
+ await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True})
if user_id in self._review_scheduler:
self._review_scheduler.cancel(user_id)
diff --git a/bot/resources/tags/bot_var.md b/bot/resources/tags/botvar.md
index 6833b3cd8..3db6ae7ac 100644
--- a/bot/resources/tags/bot_var.md
+++ b/bot/resources/tags/botvar.md
@@ -1,4 +1,4 @@
-Python allows you to set custom attributes to class instances, like your bot! By adding variables as attributes to your bot you can access them anywhere you access your bot. In the discord.py library, these custom attributes are commonly known as "bot variables" and can be a lifesaver if your bot is divided into many different files. An example on how to use custom attributes on your bot is shown below:
+Python allows you to set custom attributes to most objects, like your bot! By storing things as attributes of the bot object, you can access them anywhere you access your bot. In the discord.py library, these custom attributes are commonly known as "bot variables" and can be a lifesaver if your bot is divided into many different files. An example on how to use custom attributes on your bot is shown below:
```py
bot = commands.Bot(command_prefix="!")
diff --git a/config-default.yml b/config-default.yml
index baece5c51..a18fdafa5 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -232,7 +232,6 @@ guild:
# Watch
big_brother_logs: &BB_LOGS 468507907357409333
- talent_pool: &TALENT_POOL 534321732593647616
moderation_categories:
- *MODS_CATEGORY
@@ -305,7 +304,6 @@ guild:
duck_pond: 637821475327311927
incidents_archive: 720671599790915702
python_news: &PYNEWS_WEBHOOK 704381182279942324
- talent_pool: 569145364800602132
filter:
@@ -336,7 +334,6 @@ filter:
- *MESSAGE_LOG
- *MOD_LOG
- *STAFF_LOUNGE
- - *TALENT_POOL
role_whitelist:
- *ADMINS_ROLE
diff --git a/poetry.lock b/poetry.lock
index a4ce5d1a9..81b51b8da 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -174,7 +174,7 @@ pycparser = "*"
[[package]]
name = "cfgv"
-version = "3.3.0"
+version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
@@ -317,7 +317,7 @@ testing = ["pre-commit"]
[[package]]
name = "fakeredis"
-version = "1.5.2"
+version = "1.6.0"
description = "Fake implementation of redis API for testing purposes."
category = "main"
optional = false
@@ -329,7 +329,7 @@ six = ">=1.12"
sortedcontainers = "*"
[package.extras]
-aioredis = ["aioredis (<2)"]
+aioredis = ["aioredis"]
lua = ["lupa"]
[[package]]
@@ -437,14 +437,14 @@ flake8 = "*"
[[package]]
name = "flake8-tidy-imports"
-version = "4.3.0"
+version = "4.4.1"
description = "A flake8 plugin that helps you write tidier imports."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-flake8 = ">=3.0,<3.2.0 || >3.2.0,<4"
+flake8 = ">=3.8.0,<4"
[[package]]
name = "flake8-todo"
@@ -478,7 +478,7 @@ pyreadline = {version = "*", markers = "sys_platform == \"win32\""}
[[package]]
name = "identify"
-version = "2.2.11"
+version = "2.2.13"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -601,7 +601,7 @@ codegen = ["lxml"]
[[package]]
name = "pep8-naming"
-version = "0.12.0"
+version = "0.12.1"
description = "Check PEP-8 naming conventions, plugin for flake8"
category = "dev"
optional = false
@@ -612,6 +612,20 @@ flake8 = ">=3.9.1"
flake8-polyfill = ">=1.0.2,<2"
[[package]]
+name = "pip-licenses"
+version = "3.5.2"
+description = "Dump the software license list of Python packages installed with pip."
+category = "dev"
+optional = false
+python-versions = "~=3.6"
+
+[package.dependencies]
+PTable = "*"
+
+[package.extras]
+test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"]
+
+[[package]]
name = "platformdirs"
version = "2.2.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
@@ -636,7 +650,7 @@ dev = ["pre-commit", "tox"]
[[package]]
name = "pre-commit"
-version = "2.13.0"
+version = "2.14.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
@@ -662,6 +676,14 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
[[package]]
+name = "ptable"
+version = "0.9.2"
+description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
name = "py"
version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
@@ -851,7 +873,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "rapidfuzz"
-version = "1.4.1"
+version = "1.5.0"
description = "rapid fuzzy string matching"
category = "main"
optional = false
@@ -1015,7 +1037,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
-version = "20.7.0"
+version = "20.7.2"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@@ -1047,7 +1069,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "3.9.*"
-content-hash = "f46fe1d2d9e0621e4e06d4c2ba5f6190ec4574ac6ca809abe8bf542a3b55204e"
+content-hash = "ceddbb2621849f480f736985d71f37cebefd08a9b38bc3943a6f72706258b6ee"
[metadata.files]
aio-pika = [
@@ -1186,8 +1208,8 @@ cffi = [
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
]
cfgv = [
- {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"},
- {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"},
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
@@ -1286,8 +1308,8 @@ execnet = [
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
]
fakeredis = [
- {file = "fakeredis-1.5.2-py3-none-any.whl", hash = "sha256:f1ffdb134538e6d7c909ddfb4fc5edeb4a73d0ea07245bc69b8135fbc4144b04"},
- {file = "fakeredis-1.5.2.tar.gz", hash = "sha256:18fc1808d2ce72169d3f11acdb524a00ef96bd29970c6d34cfeb2edb3fc0c020"},
+ {file = "fakeredis-1.6.0-py3-none-any.whl", hash = "sha256:3449b306f3a85102b28f8180c24722ef966fcb1e3c744758b6f635ec80321a5c"},
+ {file = "fakeredis-1.6.0.tar.gz", hash = "sha256:11ccfc9769d718d37e45b382e64a6ba02586b622afa0371a6bd85766d72255f3"},
]
feedparser = [
{file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"},
@@ -1326,8 +1348,8 @@ flake8-string-format = [
{file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"},
]
flake8-tidy-imports = [
- {file = "flake8-tidy-imports-4.3.0.tar.gz", hash = "sha256:e66d46f58ed108f36da920e7781a728dc2d8e4f9269e7e764274105700c0a90c"},
- {file = "flake8_tidy_imports-4.3.0-py3-none-any.whl", hash = "sha256:d6e64cb565ca9474d13d5cb3f838b8deafb5fed15906998d4a674daf55bd6d89"},
+ {file = "flake8-tidy-imports-4.4.1.tar.gz", hash = "sha256:c18b3351b998787db071e766e318da1f0bd9d5cecc69c4022a69e7aa2efb2c51"},
+ {file = "flake8_tidy_imports-4.4.1-py3-none-any.whl", hash = "sha256:631a1ba9daaedbe8bb53f6086c5a92b390e98371205259e0e311a378df8c3dc8"},
]
flake8-todo = [
{file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"},
@@ -1380,8 +1402,8 @@ humanfriendly = [
{file = "humanfriendly-9.2.tar.gz", hash = "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48"},
]
identify = [
- {file = "identify-2.2.11-py2.py3-none-any.whl", hash = "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839"},
- {file = "identify-2.2.11.tar.gz", hash = "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"},
+ {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"},
+ {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"},
]
idna = [
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
@@ -1510,8 +1532,12 @@ pamqp = [
{file = "pamqp-2.3.0.tar.gz", hash = "sha256:5cd0f5a85e89f20d5f8e19285a1507788031cfca4a9ea6f067e3cf18f5e294e8"},
]
pep8-naming = [
- {file = "pep8-naming-0.12.0.tar.gz", hash = "sha256:1f9a3ecb2f3fd83240fd40afdd70acc89695c49c333413e49788f93b61827e12"},
- {file = "pep8_naming-0.12.0-py2.py3-none-any.whl", hash = "sha256:2321ac2b7bf55383dd19a6a9c8ae2ebf05679699927a3af33e60dd7d337099d3"},
+ {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.2.tar.gz", hash = "sha256:c5e984f461b34ad04dafa151d0048eb9d049e3d6439966c6440bb6b53ad077b6"},
+ {file = "pip_licenses-3.5.2-py3-none-any.whl", hash = "sha256:62deafc82d5dccea1a4cab55172706e02f228abcd67f4d53e382fcb1497e9b62"},
]
platformdirs = [
{file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"},
@@ -1522,8 +1548,8 @@ pluggy = [
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
pre-commit = [
- {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"},
- {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"},
+ {file = "pre_commit-2.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"},
+ {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"},
]
psutil = [
{file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
@@ -1555,6 +1581,9 @@ psutil = [
{file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
{file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
]
+ptable = [
+ {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"},
+]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
@@ -1679,67 +1708,67 @@ pyyaml = [
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
rapidfuzz = [
- {file = "rapidfuzz-1.4.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:72878878d6744883605b5453c382361716887e9e552f677922f76d93d622d8cb"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:56a67a5b3f783e9af73940f6945366408b3a2060fc6ab18466e5a2894fd85617"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f5d396b64f8ae3a793633911a1fb5d634ac25bf8f13d440139fa729131be42d8"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4990698233e7eda7face7c09f5874a09760c7524686045cbb10317e3a7f3225f"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a87e212855b18a951e79ec71d71dbd856d98cd2019d0c2bd46ec30688a8aa68a"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1897d2ef03f5b51bc19bdb2d0398ae968766750fa319843733f0a8f12ddde986"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:e1fc4fd219057f5f1fa40bb9bc5e880f8ef45bf19350d4f5f15ca2ce7f61c99b"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:21300c4d048798985c271a8bf1ed1611902ebd4479fcacda1a3eaaebbad2f744"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:d2659967c6ac74211a87a1109e79253e4bc179641057c64800ef4e2dc0534fdb"},
- {file = "rapidfuzz-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:26ac4bfe564c516e053fc055f1543d2b2433338806738c7582e1f75ed0485f7e"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3b485c98ad1ce3c04556f65aaab5d6d6d72121cde656d43505169c71ae956476"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:59db06356eaf22c83f44b0dded964736cbb137291cdf2cf7b4974c0983b94932"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fef95249af9a535854b617a68788c38cd96308d97ee14d44bc598cc73e986167"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:7d8c186e8270e103d339b26ef498581cf3178470ccf238dfd5fd0e47d80e4c7d"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9246b9c5c8992a83a08ac7813c8bbff2e674ad0b681f9b3fb1ec7641eff6c21f"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f58c17f7a82b1bcc2ce304942cae14287223e6b6eead7071241273da7d9b9770"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:ed708620b23a09ac52eaaec0761943c1bbc9a62d19ecd2feb4da8c3f79ef9d37"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:bdec9ae5fd8a8d4d8813b4aac3505c027b922b4033a32a7aab66a9b2f03a7b47"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:fc668fd706ad1162ce14f26ca2957b4690d47770d23609756536c918a855ced0"},
- {file = "rapidfuzz-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f9f35df5dd9b02669ff6b1d4a386607ff56982c86a7e57d95eb08c6afbab4ddd"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8427310ea29ce2968e1c6f6779ae5a458b3a4984f9150fc4d16f92b96456f848"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1430dc745476e3798742ad835f61f6e6bf5d3e9a22cf9cd0288b28b7440a9872"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1d20311da611c8f4638a09e2bc5e04b327bae010cb265ef9628d9c13c6d5da7b"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7881965e428cf6fe248d6e702e6d5857da02278ab9b21313bee717c080e443e"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f76c965f15861ec4d39e904bd65b84a39121334439ac17bfb8b900d1e6779a93"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:61167f989415e701ac379de247e6b0a21ea62afc86c54d8a79f485b4f0173c02"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:645cfb9456229f0bd5752b3eda69f221d825fbb8cbb8855433516bc185111506"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:c28be57c9bc47b3d7f484340fab1bec8ed4393dee1090892c2774a4584435eb8"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:3c94b6d3513c693f253ff762112cc4580d3bd377e4abacb96af31a3d606fbe14"},
- {file = "rapidfuzz-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:506d50a066451502ee2f8bf016bc3ba3e3b04eede7a4059d7956248e2dd96179"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:80b375098658bb3db14215a975d354f6573d3943ac2ae0c4627c7760d57ce075"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ba8f7cbd8fdbd3ae115f4484888f3cb94bc2ac7cbd4eb1ca95a3d4f874261ff8"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5fa8570720b0fdfc52f24f5663d66c52ea88ba19cb8b1ff6a39a8bc0b925b33b"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:f35c8a4c690447fd335bfd77df4da42dfea37cfa06a8ecbf22543d86dc720e12"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:27f9eef48e212d73e78f0f5ceedc62180b68f6a25fa0752d2ccfaedc3a840bec"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:31e99216e2a04aec4f281d472b28a683921f1f669a429cf605d11526623eaeed"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:f22bf7ba6eddd59764457f74c637ab5c3ed976c5fcfaf827e1d320cc0478e12b"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:c43ddb354abd00e56f024ce80affb3023fa23206239bb81916d5877cba7f2d1e"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-win32.whl", hash = "sha256:62c1f4ac20c8019ce8d481fb27235306ef3912a8d0b9a60b17905699f43ff072"},
- {file = "rapidfuzz-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:2963f356c70b710dc6337b012ec976ce2fc2b81c2a9918a686838fead6eb4e1d"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c07f301fd549b266410654850c6918318d7dcde8201350e9ac0819f0542cf147"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa4c8b6fc7e93e3a3fb9be9566f1fe7ef920735eadcee248a0d70f3ca8941341"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c200bd813bbd3b146ba0fd284a9ad314bbad9d95ed542813273bdb9d0ee4e796"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2cccc84e1f0c6217747c09cafe93164e57d3644e18a334845a2dfbdd2073cd2c"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f2033e3d61d1e498f618123b54dc7436d50510b0d18fd678d867720e8d7b2f23"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:26b7f48b3ddd9d97cf8482a88f0f6cba47ac13ff16e63386ea7ce06178174770"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bf18614f87fe3bfff783f0a3d0fad0eb59c92391e52555976e55570a651d2330"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8cb5c2502ff06028a1468bdf61323b53cc3a37f54b5d62d62c5371795b81086a"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f37f80c1541d6e0a30547261900086b8c0bac519ebc12c9cd6b61a9a43a7e195"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:c13cd1e840aa93639ac1d131fbfa740a609fd20dfc2a462d5cd7bce747a2398d"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-win32.whl", hash = "sha256:0ec346f271e96c485716c091c8b0b78ba52da33f7c6ebb52a349d64094566c2d"},
- {file = "rapidfuzz-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:5208ce1b1989a10e6fc5b5ef5d0bb7d1ffe5408838f3106abde241aff4dab08c"},
- {file = "rapidfuzz-1.4.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4fa195ea9ca35bacfa2a4319c6d4ab03aa6a283ad2089b70d2dfa0f6a7d9c1bc"},
- {file = "rapidfuzz-1.4.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:6e336cfd8103b0b38e107e01502e9d6bf7c7f04e49b970fb11a4bf6c7a932b94"},
- {file = "rapidfuzz-1.4.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c798c5b87efe8a7e63f408e07ff3bc03ba8b94f4498a89b48eaab3a9f439d52c"},
- {file = "rapidfuzz-1.4.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:bb16a10b40f5bd3c645f7748fbd36f49699a03f550c010a2c665905cc8937de8"},
- {file = "rapidfuzz-1.4.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2278001924031d9d75f821bff2c5fef565c8376f252562e04d8eec8857475c36"},
- {file = "rapidfuzz-1.4.1-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:a89d11f3b5da35fdf3e839186203b9367d56e2be792e8dccb098f47634ec6eb9"},
- {file = "rapidfuzz-1.4.1-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:f8c79cd11b4778d387366a59aa747f5268433f9d68be37b00d16f4fb08fdf850"},
- {file = "rapidfuzz-1.4.1-pp37-pypy37_pp73-win32.whl", hash = "sha256:4364db793ed4b439f9dd28a335bee14e2a828283d3b93c2d2686cc645eeafdd5"},
- {file = "rapidfuzz-1.4.1.tar.gz", hash = "sha256:de20550178376d21bfe1b34a7dc42ab107bb282ef82069cf6dfe2805a0029e26"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:670a330e90e962de5823e01e8ae1b8903af788325fbce1ef3fd5ece4d22e0ba4"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:079afafa6e6b00ee799e16d9fc6c6522132cbd7742a7a9e78bd301321e1b5ad6"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:26cb066e79c9867d313450514bb70124d392ac457640c4ec090d29eb68b75541"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:542fbe8fb4403af36bfffd53e42cb1ff3f8d969a046208373d004804072b744c"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:407a5c4d2af813e803b828b004f8686300baf298e9bf90b3388a568b1637a8dc"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:662b4021951ac9edb9a0d026820529e891cea69c11f280188c5b80fefe6ee257"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:03c97beb1c7ce5cb1d12bbb8eb87777e9a5fad23216dab78d6850cafdd3ecaf1"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:eaafa0349d47850ed2c3ae121b62e078a63daf1d533b1cd43fca0c675a85a025"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-win32.whl", hash = "sha256:f0b7e15209208ee74bc264b97e111a3c73e19336eda7255c406e56cc6fbbd384"},
+ {file = "rapidfuzz-1.5.0-cp35-cp35m-win_amd64.whl", hash = "sha256:0679af3d85082dcb27e75ea30c5047dbcc99340f38490c7d4769ae16909c246a"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a3ef319fd1162e7e38bf11259d86fc6ea3885d2abae6359e5b4dafad62592db"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:60ea1cee33a5a847aeac91a35865c6f7f35a87613df282bda2e7f984e91526f5"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2ba6ffe8ac66dbeae91a0b2cb50f4836ec16920f58746eaf46ff3e9c4f9c0ad8"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:7c101bafb27436affcaa14c631e2bf99d6a7a7860a201ce17ee98447c9c0e7f4"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a8f3f374b4e8e80516b955a1da6364c526d480311a5c6be48264cf7dc06d2fba"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f2fe161526cce52eae224c2af9ae1b9c475ae3e1001fe76024603b290bc8f719"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:8b086b2f70571c9bf16ead5f65976414f8e75a1c680220a839b8ddf005743060"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:814cd474c31db0383c69eed5b457571f63521f38829955c842b141b4835f067f"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-win32.whl", hash = "sha256:0a901aa223a4b051846cb828c33967a6f9c66b8fe0ba7e2a4dc70f6612006988"},
+ {file = "rapidfuzz-1.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f03a5fa9fe38d7f8d566bff0b66600f488d56700469bf1e5e36078f4b58290b6"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:122b7c25792eb27ca59ab23623a922a7290d881d296556d0c23da63ed1691cd5"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:73509dbfcf556233d62683aed0e5f23282ec7138eeedc3ecda2938ad8e8c969d"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6e8c4fd87361699e0cf5cf7ff075e4cd70a2698e9f914368f0c3e198c77c755c"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d627ec73d324d804af4c95909e2fa30b0e59f7efaf69264e553a0e498034404b"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c57f3b74942ae0d0869336e613cbd0760de61a462ff441095eb5fca6575cf964"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:075b8bf76dd4bbc9ccb5177806c9867424d365898415433bf88e7b8e88dc4dfe"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:8049a500b431724d283ddf97d67fe48aa67b4523d617a203c22fd9da3a496223"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:a2d84fde07c32514758d283dd1227453db3ed5372a3e9eae85d0c29b2953f252"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:0e35b9b92a955018ebd09d4d9d70f8e81a0106fe1ed04bc82e3a05166cd04ea5"},
+ {file = "rapidfuzz-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8ae7bf62f0382d13e9b36babc897742bac5e7ee04b4e5e94cd67085bfccfd2fd"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:466d9c644fa235278ef376eefb1fc4382107b07764fbc3c7280533ad9ce49bb4"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d04a8465738363d0b9ee39abb3b289e1198d1f3cbc98bc43b8e21ec8e0b21774"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2c1ce8e8419ac8462289a6e021b8802701ea0f111ebde7607ba3c9588c3d6f30"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:f44564a29e96af0925e68733859d8247a692968034e1b37407d9cfa746d3a853"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d2d1bea50f54387bc1e82b93f6e3a433084e0fa538a7ada8e4d4d7200bae4b83"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b409f0f86a316b6132253258185c7b011e779ed2170d1ad83c79515fea7d78c8"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:bf5a6f4f2eb44f32271e9c2d1e46b657764dbd1b933dd84d7c0433eab48741f8"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bbdee2e3c2cee9c59e1d1a3f351760a1b510e96379d14ba2fa2484a79f56d0ea"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-win32.whl", hash = "sha256:575a0eceaf84632f2014fd55a42a0621e448115adf6fcbc2b0e5c7ae1c18b501"},
+ {file = "rapidfuzz-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:cd6603b94e2a3d56d143a5100f8f3c1d29ad8f5416bdc2a25b079f96eee3c306"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3fa261479e3828eff1f3d0265def8d0d893f2e2f90692d5dae96b3f4ae44d69e"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7a386fe0aad7e89b5017768492ea085d241c32f6dc5a6774b0a309d28f61e720"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68156a67d541bb4584cb31e366fb7de9326f5b77ed07f9882e9b9aaa40b2e5b8"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b62b2a2d2532d357d1b970107a90e85305bdd8e302995dd251f67a19495033f5"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:190b48ba8e3fbcb1cfc522300dbd6a007f50c13cd71002c95bd3946a63b749f6"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:51f9ac3316e713b4a10554a4d6b75fe6f802dd9b4073082cc98968ace6377cac"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e00198aa7ca8408616d9821501ff90157c429c952d55a2a53987a9b064f73d49"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5784c24e2de539064d8d5ce3f68756630b54fc33af31e054373a65bbed68823a"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:712a4d510c466d6ca75138dad53a1cbd8db0da4bbfa5fc431fcebb0a426e5323"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:2647e00e2211ed741aecb4e676461b7202ce46d536c3439ede911b088432b7a4"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-win32.whl", hash = "sha256:0b77ca0dacb129e878c2583295b76e12da890bd091115417d23b4049b02c2566"},
+ {file = "rapidfuzz-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:dec0d429d117ffd7df1661e5f6ca56bfb6806e117be0b75b5d414df43aa4b6d5"},
+ {file = "rapidfuzz-1.5.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a533d17d177d11b7c177c849adb728035621462f6ce2baaeb9cf1f42ba3e326c"},
+ {file = "rapidfuzz-1.5.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ac9a2d5a47a4a4eab060882a162d3626889abdec69f899a59fe7b9e01ce122c9"},
+ {file = "rapidfuzz-1.5.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0e6e2f02bb67a35d75a5613509bb49f0050c0ec4471a9af14da3ad5488d6d5ff"},
+ {file = "rapidfuzz-1.5.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:8c61ced6729146e695ecad403165bf3a07e60b8e8a18df91962b3abf72aae6d5"},
+ {file = "rapidfuzz-1.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:360415125e967d8682291f00bcea311c738101e0aee4cb90e5572d7e54483f0d"},
+ {file = "rapidfuzz-1.5.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:2fb9d47fc16a2e8f5e900c8334d823a7307148ea764321f861b876f85a880d57"},
+ {file = "rapidfuzz-1.5.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:2134ac91e8951d42c9a7de131d767580b8ac50820475221024e5bd63577a376f"},
+ {file = "rapidfuzz-1.5.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:04c4fd372e858f25e0898ba27b5bb7ed8dc528b0915b7aa02d20237e9cdd4feb"},
+ {file = "rapidfuzz-1.5.0.tar.gz", hash = "sha256:141ee381c16f7e58640ef1f1dbf76beb953d248297a7165f7ba25d81ac1161c7"},
]
redis = [
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
@@ -1837,8 +1866,8 @@ urllib3 = [
{file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"},
]
virtualenv = [
- {file = "virtualenv-20.7.0-py2.py3-none-any.whl", hash = "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"},
- {file = "virtualenv-20.7.0.tar.gz", hash = "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094"},
+ {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"},
+ {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"},
]
yarl = [
{file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"},
diff --git a/pyproject.toml b/pyproject.toml
index 2ae79f9e4..23cbba19b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ flake8-todo = "~=0.7"
pep8-naming = "~=0.9"
pre-commit = "~=2.1"
taskipy = "~=1.7.0"
+pip-licenses = "~=3.5.2"
python-dotenv = "~=0.17.1"
pytest = "~=6.2.4"
pytest-cov = "~=2.12.1"