aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/constants.py2
-rw-r--r--bot/exts/events/advent_of_code/_cog.py56
-rw-r--r--bot/exts/fun/madlibs.py148
-rw-r--r--bot/exts/utilities/epoch.py135
-rw-r--r--bot/monkey_patches.py2
-rw-r--r--bot/resources/fun/madlibs_templates.json135
-rw-r--r--poetry.lock80
-rw-r--r--pyproject.toml2
8 files changed, 473 insertions, 87 deletions
diff --git a/bot/constants.py b/bot/constants.py
index 01f825a0..3b426c47 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -197,7 +197,7 @@ class Emojis:
# These icons are from Github's repo https://github.com/primer/octicons/
issue_open = "<:IssueOpen:852596024777506817>"
- issue_closed = "<:IssueClosed:852596024739758081>"
+ issue_closed = "<:IssueClosed:927326162861039626>"
issue_draft = "<:IssueDraft:852596025147523102>" # Not currently used by Github, but here for future.
pull_request_open = "<:PROpen:852596471505223781>"
pull_request_closed = "<:PRClosed:852596024732286976>"
diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py
index a9625153..3acfef39 100644
--- a/bot/exts/events/advent_of_code/_cog.py
+++ b/bot/exts/events/advent_of_code/_cog.py
@@ -11,12 +11,13 @@ from discord.ext import commands, tasks
from bot.bot import Bot
from bot.constants import (
- AdventOfCode as AocConfig, Channels, Client, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS
+ AdventOfCode as AocConfig, Channels, Client, Colours, Emojis, Month, PYTHON_PREFIX, Roles, WHITELISTED_CHANNELS
)
from bot.exts.events.advent_of_code import _helpers
from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView
from bot.utils import members
from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role
+from bot.utils.exceptions import MovedCommandError
from bot.utils.extensions import invoke_help_command
log = logging.getLogger(__name__)
@@ -95,7 +96,9 @@ class AdventOfCode(commands.Cog):
# Only give the role to people who have completed all 50 stars
continue
- member_id = aoc_name_to_member_id.get(member_aoc_info["name"], None)
+ aoc_name = member_aoc_info["name"] or f"Anonymous #{member_aoc_info['id']}"
+
+ member_id = aoc_name_to_member_id.get(aoc_name)
if not member_id:
log.debug(f"Could not find member_id for {member_aoc_info['name']}, not giving role.")
continue
@@ -137,45 +140,17 @@ class AdventOfCode(commands.Cog):
@commands.guild_only()
@adventofcode_group.command(
name="subscribe",
- aliases=("sub", "notifications", "notify", "notifs"),
- brief="Notifications for new days"
+ aliases=("sub", "notifications", "notify", "notifs", "unsubscribe", "unsub"),
+ help=f"NOTE: This command has been moved to {PYTHON_PREFIX}subscribe",
)
@whitelist_override(channels=AOC_WHITELIST)
async def aoc_subscribe(self, ctx: commands.Context) -> None:
- """Assign the role for notifications about new days being ready."""
- current_year = datetime.now().year
- if current_year != AocConfig.year:
- await ctx.send(f"You can't subscribe to {current_year}'s Advent of Code announcements yet!")
- return
-
- role = ctx.guild.get_role(AocConfig.role_id)
- unsubscribe_command = f"{ctx.prefix}{ctx.command.root_parent} unsubscribe"
-
- if role not in ctx.author.roles:
- await ctx.author.add_roles(role)
- await ctx.send(
- "Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. "
- f"You can run `{unsubscribe_command}` to disable them again for you."
- )
- else:
- await ctx.send(
- "Hey, you already are receiving notifications about new Advent of Code tasks. "
- f"If you don't want them any more, run `{unsubscribe_command}` instead."
- )
-
- @in_month(Month.DECEMBER)
- @commands.guild_only()
- @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
- @whitelist_override(channels=AOC_WHITELIST)
- async def aoc_unsubscribe(self, ctx: commands.Context) -> None:
- """Remove the role for notifications about new days being ready."""
- role = ctx.guild.get_role(AocConfig.role_id)
+ """
+ Deprecated role command.
- if role in ctx.author.roles:
- await ctx.author.remove_roles(role)
- await ctx.send("Okay! You have been __unsubscribed__ from notifications about new Advent of Code tasks.")
- else:
- await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.")
+ This command has been moved to bot, and will be removed in the future.
+ """
+ raise MovedCommandError(f"{PYTHON_PREFIX}subscribe")
@adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day")
@whitelist_override(channels=AOC_WHITELIST)
@@ -214,9 +189,10 @@ class AdventOfCode(commands.Cog):
async def join_leaderboard(self, ctx: commands.Context) -> None:
"""DM the user the information for joining the Python Discord leaderboard."""
current_date = datetime.now()
- if (
- current_date.month not in (Month.NOVEMBER, Month.DECEMBER) and current_date.year != AocConfig.year or
- current_date.month != Month.JANUARY and current_date.year != AocConfig.year + 1
+ allowed_months = (Month.NOVEMBER.value, Month.DECEMBER.value)
+ if not (
+ current_date.month in allowed_months and current_date.year == AocConfig.year or
+ current_date.month == Month.JANUARY.value and current_date.year == AocConfig.year + 1
):
# Only allow joining the leaderboard in the run up to AOC and the January following.
await ctx.send(f"The Python Discord leaderboard for {current_date.year} is not yet available!")
diff --git a/bot/exts/fun/madlibs.py b/bot/exts/fun/madlibs.py
new file mode 100644
index 00000000..21708e53
--- /dev/null
+++ b/bot/exts/fun/madlibs.py
@@ -0,0 +1,148 @@
+import json
+from asyncio import TimeoutError
+from pathlib import Path
+from random import choice
+from typing import TypedDict
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours, NEGATIVE_REPLIES
+
+TIMEOUT = 60.0
+
+
+class MadlibsTemplate(TypedDict):
+ """Structure of a template in the madlibs JSON file."""
+
+ title: str
+ blanks: list[str]
+ value: list[str]
+
+
+class Madlibs(commands.Cog):
+ """Cog for the Madlibs game."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.templates = self._load_templates()
+ self.edited_content = {}
+ self.checks = set()
+
+ @staticmethod
+ def _load_templates() -> list[MadlibsTemplate]:
+ madlibs_stories = Path("bot/resources/fun/madlibs_templates.json")
+
+ with open(madlibs_stories) as file:
+ return json.load(file)
+
+ @staticmethod
+ def madlibs_embed(part_of_speech: str, number_of_inputs: int) -> discord.Embed:
+ """Method to generate an embed with the game information."""
+ madlibs_embed = discord.Embed(title="Madlibs", color=Colours.python_blue)
+
+ madlibs_embed.add_field(
+ name="Enter a word that fits the given part of speech!",
+ value=f"Part of speech: {part_of_speech}\n\nMake sure not to spam, or you may get auto-muted!"
+ )
+
+ madlibs_embed.set_footer(text=f"Inputs remaining: {number_of_inputs}")
+
+ return madlibs_embed
+
+ @commands.Cog.listener()
+ async def on_message_edit(self, _: discord.Message, after: discord.Message) -> None:
+ """A listener that checks for message edits from the user."""
+ for check in self.checks:
+ if check(after):
+ break
+ else:
+ return
+
+ self.edited_content[after.id] = after.content
+
+ @commands.command()
+ @commands.max_concurrency(1, per=commands.BucketType.user)
+ async def madlibs(self, ctx: commands.Context) -> None:
+ """
+ Play Madlibs with the bot!
+
+ Madlibs is a game where the player is asked to enter a word that
+ fits a random part of speech (e.g. noun, adjective, verb, plural noun, etc.)
+ a random amount of times, depending on the story chosen by the bot at the beginning.
+ """
+ random_template = choice(self.templates)
+
+ def author_check(message: discord.Message) -> bool:
+ return message.channel.id == ctx.channel.id and message.author.id == ctx.author.id
+
+ self.checks.add(author_check)
+
+ loading_embed = discord.Embed(
+ title="Madlibs", description="Loading your Madlibs game...", color=Colours.python_blue
+ )
+ original_message = await ctx.send(embed=loading_embed)
+
+ submitted_words = {}
+
+ for i, part_of_speech in enumerate(random_template["blanks"]):
+ inputs_left = len(random_template["blanks"]) - i
+
+ madlibs_embed = self.madlibs_embed(part_of_speech, inputs_left)
+ await original_message.edit(embed=madlibs_embed)
+
+ try:
+ message = await self.bot.wait_for(event="message", check=author_check, timeout=TIMEOUT)
+ except TimeoutError:
+ timeout_embed = discord.Embed(
+ title=choice(NEGATIVE_REPLIES),
+ description="Uh oh! You took too long to respond!",
+ color=Colours.soft_red
+ )
+
+ await ctx.send(ctx.author.mention, embed=timeout_embed)
+
+ for msg_id in submitted_words:
+ self.edited_content.pop(msg_id, submitted_words[msg_id])
+
+ self.checks.remove(author_check)
+
+ return
+
+ submitted_words[message.id] = message.content
+
+ blanks = [self.edited_content.pop(msg_id, submitted_words[msg_id]) for msg_id in submitted_words]
+
+ self.checks.remove(author_check)
+
+ story = []
+ for value, blank in zip(random_template["value"], blanks):
+ story.append(f"{value}__{blank}__")
+
+ # In each story template, there is always one more "value"
+ # (fragment from the story) than there are blanks (words that the player enters)
+ # so we need to compensate by appending the last line of the story again.
+ story.append(random_template["value"][-1])
+
+ story_embed = discord.Embed(
+ title=random_template["title"],
+ description="".join(story),
+ color=Colours.bright_green
+ )
+
+ story_embed.set_footer(text=f"Generated for {ctx.author}", icon_url=ctx.author.display_avatar.url)
+
+ await ctx.send(embed=story_embed)
+
+ @madlibs.error
+ async def handle_madlibs_error(self, ctx: commands.Context, error: commands.CommandError) -> None:
+ """Error handler for the Madlibs command."""
+ if isinstance(error, commands.MaxConcurrencyReached):
+ await ctx.send("You are already playing Madlibs!")
+ error.handled = True
+
+
+def setup(bot: Bot) -> None:
+ """Load the Madlibs cog."""
+ bot.add_cog(Madlibs(bot))
diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py
new file mode 100644
index 00000000..03758af0
--- /dev/null
+++ b/bot/exts/utilities/epoch.py
@@ -0,0 +1,135 @@
+from typing import Optional, Union
+
+import arrow
+import discord
+from dateutil import parser
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.utils.extensions import invoke_help_command
+
+# https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
+STYLES = {
+ "Epoch": ("",),
+ "Short Time": ("t", "h:mm A",),
+ "Long Time": ("T", "h:mm:ss A"),
+ "Short Date": ("d", "MM/DD/YYYY"),
+ "Long Date": ("D", "MMMM D, YYYY"),
+ "Short Date/Time": ("f", "MMMM D, YYYY h:mm A"),
+ "Long Date/Time": ("F", "dddd, MMMM D, YYYY h:mm A"),
+ "Relative Time": ("R",)
+}
+DROPDOWN_TIMEOUT = 60
+
+
+class DateString(commands.Converter):
+ """Convert a relative or absolute date/time string to an arrow.Arrow object."""
+
+ async def convert(self, ctx: commands.Context, argument: str) -> Union[arrow.Arrow, Optional[tuple]]:
+ """
+ Convert a relative or absolute date/time string to an arrow.Arrow object.
+
+ Try to interpret the date string as a relative time. If conversion fails, try to interpret it as an absolute
+ time. Tokens that are not recognised are returned along with the part of the string that was successfully
+ converted to an arrow object. If the date string cannot be parsed, BadArgument is raised.
+ """
+ try:
+ return arrow.utcnow().dehumanize(argument)
+ except (ValueError, OverflowError):
+ try:
+ dt, ignored_tokens = parser.parse(argument, fuzzy_with_tokens=True)
+ except parser.ParserError:
+ raise commands.BadArgument(f"`{argument}` Could not be parsed to a relative or absolute date.")
+ except OverflowError:
+ raise commands.BadArgument(f"`{argument}` Results in a date outside of the supported range.")
+ return arrow.get(dt), ignored_tokens
+
+
+class Epoch(commands.Cog):
+ """Convert an entered time and date to a unix timestamp."""
+
+ @commands.command(name="epoch")
+ async def epoch(self, ctx: commands.Context, *, date_time: DateString = None) -> None:
+ """
+ Convert an entered date/time string to the equivalent epoch.
+
+ **Relative time**
+ Must begin with `in...` or end with `...ago`.
+ Accepted units: "seconds", "minutes", "hours", "days", "weeks", "months", "years".
+ eg `.epoch in a month 4 days and 2 hours`
+
+ **Absolute time**
+ eg `.epoch 2022/6/15 16:43 -04:00`
+ Absolute times must be entered in descending orders of magnitude.
+ If AM or PM is left unspecified, the 24-hour clock is assumed.
+ Timezones are optional, and will default to UTC. The following timezone formats are accepted:
+ Z (UTC)
+ ±HH:MM
+ ±HHMM
+ ±HH
+
+ Times in the dropdown are shown in UTC
+ """
+ if not date_time:
+ await invoke_help_command(ctx)
+ return
+
+ if isinstance(date_time, tuple):
+ # Remove empty strings. Strip extra whitespace from the remaining items
+ ignored_tokens = list(map(str.strip, filter(str.strip, date_time[1])))
+ date_time = date_time[0]
+ if ignored_tokens:
+ await ctx.send(f"Could not parse the following token(s): `{', '.join(ignored_tokens)}`")
+ await ctx.send(f"Date and time parsed as: `{date_time.format(arrow.FORMAT_RSS)}`")
+
+ epoch = int(date_time.timestamp())
+ view = TimestampMenuView(ctx, self._format_dates(date_time), epoch)
+ original = await ctx.send(f"`{epoch}`", view=view)
+ await view.wait() # wait until expiration before removing the dropdown
+ await original.edit(view=None)
+
+ @staticmethod
+ def _format_dates(date: arrow.Arrow) -> list[str]:
+ """
+ Return a list of date strings formatted according to the discord timestamp styles.
+
+ These are used in the description of each style in the dropdown
+ """
+ date = date.to('utc')
+ formatted = [str(int(date.timestamp()))]
+ formatted += [date.format(format[1]) for format in list(STYLES.values())[1:7]]
+ formatted.append(date.humanize())
+ return formatted
+
+
+class TimestampMenuView(discord.ui.View):
+ """View for the epoch command which contains a single `discord.ui.Select` dropdown component."""
+
+ def __init__(self, ctx: commands.Context, formatted_times: list[str], epoch: int):
+ super().__init__(timeout=DROPDOWN_TIMEOUT)
+ self.ctx = ctx
+ self.epoch = epoch
+ self.dropdown: discord.ui.Select = self.children[0]
+ for label, date_time in zip(STYLES.keys(), formatted_times):
+ self.dropdown.add_option(label=label, description=date_time)
+
+ @discord.ui.select(placeholder="Select the format of your timestamp")
+ async def select_format(self, _: discord.ui.Select, interaction: discord.Interaction) -> discord.Message:
+ """Drop down menu which contains a list of formats which discord timestamps can take."""
+ selected = interaction.data["values"][0]
+ if selected == "Epoch":
+ return await interaction.response.edit_message(content=f"`{self.epoch}`")
+ return await interaction.response.edit_message(content=f"`<t:{self.epoch}:{STYLES[selected][0]}>`")
+
+ async def interaction_check(self, interaction: discord.Interaction) -> bool:
+ """Check to ensure that the interacting user is the user who invoked the command."""
+ if interaction.user != self.ctx.author:
+ embed = discord.Embed(description="Sorry, but this dropdown menu can only be used by the original author.")
+ await interaction.response.send_message(embed=embed, ephemeral=True)
+ return False
+ return True
+
+
+def setup(bot: Bot) -> None:
+ """Load the Epoch cog."""
+ bot.add_cog(Epoch())
diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py
index 19965c19..925d3206 100644
--- a/bot/monkey_patches.py
+++ b/bot/monkey_patches.py
@@ -6,7 +6,7 @@ from discord import Forbidden, http
from discord.ext import commands
log = logging.getLogger(__name__)
-MESSAGE_ID_RE = re.compile(r'(?P<message_id>[0-9]{15,20})$')
+MESSAGE_ID_RE = re.compile(r"(?P<message_id>[0-9]{15,20})$")
class Command(commands.Command):
diff --git a/bot/resources/fun/madlibs_templates.json b/bot/resources/fun/madlibs_templates.json
new file mode 100644
index 00000000..0023d48f
--- /dev/null
+++ b/bot/resources/fun/madlibs_templates.json
@@ -0,0 +1,135 @@
+[
+ {
+ "title": "How To Cross a Piranha-Infested River",
+ "blanks": ["foreign country", "adverb", "adjective", "animal",
+ "verb ending in 'ing'", "verb", "verb ending in 'ing'",
+ "adverb", "adjective", "a place", "type of liquid", "part of the body", "verb"],
+ "value": ["If you are traveling in ", " and find yourself having to cross a piranha-filled river, here's how to do it ",
+ ": \n* Piranhas are more ", " during the day, so cross the river at night.\n* Avoid areas with netted ",
+ " traps--piranhas may be ", " there looking to ", " them!\n* When "," the river, swim ",
+ ". You don't want to wake them up and make them ", "!\n* Whatever you do, if you have an open wound, try to find another way to get back to the ",
+ ". Piranhas are attracted to fresh ", " and will most likely take a bite out of your ", " if you ", " in the water!"]
+ },
+ {
+ "title": "Three Little Pigs",
+ "blanks": ["adjective", "verb", "verb", "verb", "plural noun", "verb", "verb", "past tense verb", "plural noun", "adjective", "verb",
+ "plural noun", "noun", "verb", "past tense verb", "noun", "noun", "noun", "past tense verb", "adjective", "past tense verb",
+ "past tense verb", "noun", "past tense verb"],
+ "value": ["Once up a time, there were three ", " pigs. One day, their mother said, \"You are all grown up and must ", " on your own.\" So they left to ",
+ " their houses. The first little pig wanted only to ", " all day and quickly built his house out of ", ". The second little pig wanted to ",
+ " and ", " all day so he ", " his house with ", ". The third ", " pig knew the wolf lived nearby and worked hard to ", " his house out of ",
+ ". One day, the wolf knocked on the first pig's ", ". \"Let me in or I'll ", " your house down!\" The pig didn't, so the wolf ", " down the ",
+ ". The wolf knocked on the second pig's ", ". \"Let me in or I'll blow your ", " down!\" The pig didn't, so the wolf ",
+ " down the house. Then the wolf knocked on the third ", " pig's door. \"Let me in or I'll blow your house down!\" The little pig didn't so the wolf ",
+ " and ", ". He could not blow the house down. All the pigs went to live in the ", " house and they all ", " happily ever after."]
+ },
+ {
+ "title": "Talk Like a Pirate",
+ "blanks": ["noun", "adjective", "verb", "adverb", "noun", "adjective", "plural noun", "plural noun", "plural noun", "part of the body", "noun",
+ "noun", "noun", "noun", "part of the body"],
+ "value": ["Ye can always pretend to be a bloodthirsty ", ", threatening everyone by waving yer ", " sword in the air, but until ye learn to ",
+ " like a pirate, ye'll never be ", " accepted as an authentic ", ". So here's what ye do: Cleverly work into yer daily conversations ",
+ " pirate phrases such as \"Ahoy there, ", "Avast, ye ", ",\" and \"Shiver me ", ".\" Remember to drop all yer gs when ye say such words as sailin', spittin', and fightin'. This will give ye a/an ",
+ " start to being recognized as a swashbucklin' ", ". Once ye have the lingo down pat, it helps to wear a three-cornered ", " on yer head, stash a/an ",
+ " in yer pants, and keep a/an ", " perched atop yer ", ". Aye, now ye be a real pirate!"]
+ },
+ {
+ "title": "How to Date the Coolest Guy/Girl in School",
+ "blanks": ["plural noun", "adverb", "verb", "article of clothing", "body part", "adjective", "noun", "plural noun", "another body part", "plural noun",
+ "another body part", "noun", "noun", "verb ending in 'ing'", "adjective", "adjective", "verb"],
+ "value": ["It's simple. Turn the ", ". Make him/her want ", " to date you. Make sure you're always dressed to ", ". Each and every day, wear a/an ",
+ " that you know shows off your ", " to ", " advantage and make your ", " look like a million ", ". Even if the two of you make meaningful ",
+ " contact, don't admit it. No hugs or ", ". Just shake his/her ", " firmly. And remember, when he/she asks you out, even though a chill may run down your ",
+ " and you can't stop your ", " from ", ", just play it ", ". Take a long pause before answering in a very ", " voice. \"I'll have to ",
+ " it over.\""]
+ },
+ {
+ "title": "The Fun Park",
+ "blanks": ["adjective", "plural noun", "noun", "adverb", "number", "past tense verb", "adjective ending in -est", "past tense verb", "adverb", "adjective"],
+ "value": ["Today, my fabulous camp group went to a(an) ", " amusement park. It was a fun park with lots of cool ",
+ " and enjoyable play structures. When we got there, my kind counselor shouted loudly, \"Everybody off the ",
+ ".\" My counselor handed out yellow tickets, and we scurried in. I was so excited! I couldn't figure out what exciting thing to do first. I saw a scary roller coaster I really liked so, I ",
+ " ran over to get in the long line that had about ", " people in it. when I finally got on the roller coaster I was ",
+ ". In fact, I was so nervous my two knees were knocking together. This was the ", " ride I had ever been on! In about two minutes I heard the crank and grinding of the gears. Thats when the ride began! When I got to the bottom, I was a little ",
+ " but I was proud of myself. The rest of the day went ", ". It was a ", " day at the fun park."]
+ },
+ {
+ "title": "A Spooky Campfire Story",
+ "blanks": ["adjective", "adjective", "number", "adjective", "animal", "noun", "animal", "name", "verb", "adjective", "adjective"],
+ "value": ["Every summer, I get totally amped and ", " to go camping in the deep, ", " forests. It's good to get away from it all - but not too far, like getting lost! Last year, my friend and I went hiking and got lost for ",
+ " hour(s). We started off on a(n) ", " adventure, but we kept losing the trail. Night began to fall, and when we heard the howls of a ",
+ ", we began to panic. It was getting darker and our flashlights were running on ", ". I'm sure glad my pet ", ", ", ", was with us. He is one gifted creature, because he was able to guide us back by ",
+ " the ", " s'mores by the campfire. This year, before setting off on an ", " journey, I'll be sure to have working flashlights - and of course, my gifted pet!"]
+ },
+ {
+ "title": "Weird News",
+ "blanks": ["noun", "place", "verb ending in ing", "noun", "name", "verb", "noun", "verb", "noun", "part of body", "type of liquid", "place", " past tense verb ", "foreign country", "verb", "noun", "past tense verb", "adjective", "verb", "noun", "plural noun"],
+ "value": ["A ", " in a ", " was arrested this morning after he was caught ", " in front of ", ". ", " had a history of ", ", but no one - not even his ", "- ever imagined he'd ", " with a ", " stuck in his ", ". After drinking a ", ", cops followed him to a ",
+ " where he reportedly ", " in the fry machine. Later, a woman from ", " was charged with a similar crime. But rather than ", " with a ", ", she ", " with a ", " dog. Either way, we imagine that after witnessing him ", " with a ", " there are probably a whole lot of ",
+ " that are going to need some therapy!"]
+ },
+ {
+ "title": "All About Vampires",
+ "blanks": ["adjective", "adjective", "body part", "verb ending in -ing", "verb", "verb", "verb", "noun", "verb", "verb", "body part", "verb (ending with -s)", "verb (ending with -s)", "verb", "noun", "body part", "adjective"],
+ "value": ["Vampires are ", "! They have ", " ", " for ", " blood. The sun can ", " vampires, so they only ", " at night and ", " during the day. Vampires also don't like ", " so ", " it or ", " it around your ",
+ " to keep them away. If a vampire ", "s a person and ", "s their blood, they become a vampire, too. The only way to ", " a vampire is with a ", " through the ", ", but ",
+ " luck getting close enough to one!"]
+ },
+ {
+ "title": "Our Cafeteria",
+ "blanks": ["adjective", "verb", "adjective", "noun", "verb", "adjective", "noun", "adjective", "adjective", "noun", "noun"],
+ "value": ["Our school cafeteria has really ", " food. Just thinking about it makes my stomach ", ". The spaghetti is ", " and tastes like ", ". One day, I swear one of my meatballs started to ",
+ "! The turkey tacos are totally ", " and look kind of like old ", ". My friend Dana actually likes the meatloaf, even though it's ", " and ", ". I call it \"Mystery Meatloaf\" and think it's really made out of ",
+ ". My dad said he'd make my lunches, but the first day, he made me a sandwich out of ", " and peanut butter! I think I'd rather take my chances with the cafeteria!"]
+ },
+ {
+ "title": "Trip to the Park",
+ "blanks": ["adjective", "adjective", "noun", "adjective", "adjective", "verb", "verb", "verb", "adjective", "verb"],
+ "value": ["Yesterday, my friend and I went to the park. On our way to the ", " park, we saw big ", " balloons tied to a ", ". Once we got to the ", " park, the sky turned ",
+ ". It started to ", " and ", ". My friend and I ", " all the way home. Tomorrow we will try to go to the ", " park again and hopefully it doesn't ", "!"]
+ },
+ {
+ "title": "A Scary Halloween Story",
+ "blanks": ["adjective", "name", "adjective", "noun", "verb", "animal", "adjective", "name", "adjective", "noun", "noun"],
+ "value": ["They say my school is haunted; my ", " friend ", " says they saw a ", " ", " floating at the end of the hall near the cafeteria. Some say if you ",
+ " down that hallway at night, you'll hear a ", " growling deeply. My ", " friend ", " saw a ", " ", " slithering under the tables once. I hope I never see any ",
+ " crawling; eating lunch there is scary enough!"]
+ },
+ {
+ "title": "Zombie Picnic",
+ "blanks": ["verb", "verb", "body part", "body part", "body part", "noun", "adjective", "type of liquid", "body part", "verb", "adjective", "body part", "body part"],
+ "value": ["If zombies had a picnic, what would they ", " to eat? Everybody knows zombies love to ", " ", ", but did you know they also enjoy ", " and even ",
+ "? The best ", " for a zombie picnic is when the moon is ", ". At least one zombie will bring ", " to drink, and it's not a picnic without ",
+ " with extra flesh on top. After eating, zombies will ", " ", " games like kick the ", " and ", " toss. What fun!"]
+ },
+ {
+ "title": "North Pole",
+ "blanks": ["plural noun", "adjective", "plural noun", "verb", "verb", "number", "noun", "adjective", "plural noun", "plural noun", "animal", "verb", "verb", "plural noun"],
+ "value": ["Santa, Mrs. Claus, and the ", " live at the North pole. The weather is always ", " there, but the ", " ", " toys for Santa to ",
+ " to children on Christmas, so holiday cheer lasts year-round there. There's no land at the North Pole; instead there is a ", "-inch thick sheet of ",
+ " there, ", " enough to hold Santa's Village! The ", " help load Santa's sleigh with ", ", and Santa's ", " ", " his sleigh on Christmas Eve to ",
+ " ", " to children around the entire world."]
+ },
+ {
+ "title": "Snowstorm!",
+ "blanks": ["plural noun", "adjective", "noun", "noun", "adjective", "adjective", "adjective", "noun", "noun", "adjective"],
+ "value": ["Weather plays an important part in our ", " everyday. What is weather, you ask? According to ", " scientists, who are known as meteorologists, weather is what the ",
+ " is like at any given time of the ", ". It doesn't matter if the air is ", " or ", ", it's all weather. When vapors in ", " clouds condense, we have ",
+ " and snow. A lot of ", " means a ", " snowstorm!"]
+ },
+ {
+ "title": "Learning About History",
+ "blanks": ["adjective", "noun", "nouns", "adjective", "nouns", "nouns", "animals", "nouns", "nouns", "number", "number", "nouns", "adjective", "nouns"],
+ "value": ["History is ", " because we learn about ", " and ", " that happened long ago. I can't believe people used to dress in ", " clothing and kids played with ",
+ " and ", " instead of video games. Also, before cars were invented, people actually rode ", "! People read ", " instead of computers and tablets, and sent messages via ",
+ " that took ", " days to arrive. I wonder how kids will view my life in ", " year(s); maybe they will ride flying cars to school and play with ", " and ", " ", "!"]
+ },
+ {
+ "title": "Star Wars",
+ "blanks": ["adjective", "noun", "adjective", "noun; place", "adjective", "adjective", "adjective", "adjective", "plural noun", "adjective", "plural noun", "plural noun", "adjective",
+ "noun", "verb (ending with -s)", "adjective", "verb", "plural noun; type of job", "adjective", "verb", "adjective"],
+ "value": ["Star Wars is a ", " ", " of ", " versus evil in a ", " far far away. There are ", " battles between ", " ships in ", " space and ", " duels with ", " called ",
+ " sabers. ", " called \"droids\" are helpers and ", " to the heroes. A ", " power called The ", " ", " people to do ", " things, like ", " ", " use The Force for the ",
+ " side and the Sith ", " it for the ", " side."]
+ }
+]
diff --git a/poetry.lock b/poetry.lock
index 6a83efed..68bfc43a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -193,6 +193,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"]
[package.source]
type = "url"
url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"
+
[[package]]
name = "distlib"
version = "0.3.4"
@@ -473,11 +474,11 @@ flake8-polyfill = ">=1.0.2,<2"
[[package]]
name = "pillow"
-version = "8.4.0"
+version = "9.0.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[[package]]
name = "pip-licenses"
@@ -823,7 +824,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
-content-hash = "e3682fd5b518ada5066b36015210f00d223c5485c24e4e7c377e371fe6ef0a0d"
+content-hash = "e824a5fa909d43e861478178ad7e77ee04be05a60cd3028bda8bd4754c848616"
[metadata.files]
aiodns = [
@@ -1237,47 +1238,38 @@ pep8-naming = [
{file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"},
]
pillow = [
- {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"},
- {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"},
- {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"},
- {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"},
- {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"},
- {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"},
- {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"},
- {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"},
- {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"},
- {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"},
- {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"},
- {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"},
- {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"},
- {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"},
- {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"},
- {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"},
- {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"},
- {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"},
- {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"},
- {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"},
- {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"},
- {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"},
- {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"},
- {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"},
- {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"},
- {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"},
- {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"},
- {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"},
- {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"},
- {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"},
- {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"},
- {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"},
- {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"},
- {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"},
- {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"},
- {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"},
- {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"},
- {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"},
- {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"},
- {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"},
- {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"},
+ {file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"},
+ {file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"},
+ {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"},
+ {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"},
+ {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"},
+ {file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"},
+ {file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"},
+ {file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"},
+ {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"},
+ {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"},
+ {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"},
+ {file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"},
+ {file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"},
+ {file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"},
+ {file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"},
+ {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"},
+ {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"},
+ {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"},
+ {file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"},
+ {file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"},
+ {file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"},
+ {file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"},
+ {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"},
+ {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"},
+ {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"},
+ {file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"},
+ {file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"},
+ {file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"},
+ {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"},
+ {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"},
+ {file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"},
+ {file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"},
]
pip-licenses = [
{file = "pip-licenses-3.5.3.tar.gz", hash = "sha256:f44860e00957b791c6c6005a3328f2d5eaeee96ddb8e7d87d4b0aa25b02252e4"},
diff --git a/pyproject.toml b/pyproject.toml
index 2a216209..7d3f0a5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,7 +13,7 @@ aioredis = "~1.3"
rapidfuzz = "~=1.4"
arrow = "~=1.1.0"
beautifulsoup4 = "~=4.9"
-pillow = "~=8.1"
+pillow = "~=9.0"
sentry-sdk = "~=0.19"
PyYAML = "~=5.4"
async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"}