From 0712a27181842a0af52f8051b5601a75874feeca Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:57:04 +0200 Subject: [PATCH 01/18] Migrated advent of code cog to new extension format --- bot/cogs/adventofcode.py | 311 ------------------------ bot/extensions/adventofcode/__init__.py | 9 + bot/extensions/adventofcode/commands.py | 216 ++++++++++++++++ bot/extensions/adventofcode/tasks.py | 53 ++++ cli.py | 1 + 5 files changed, 279 insertions(+), 311 deletions(-) delete mode 100644 bot/cogs/adventofcode.py create mode 100644 bot/extensions/adventofcode/__init__.py create mode 100644 bot/extensions/adventofcode/commands.py create mode 100644 bot/extensions/adventofcode/tasks.py diff --git a/bot/cogs/adventofcode.py b/bot/cogs/adventofcode.py deleted file mode 100644 index e2afb7be..00000000 --- a/bot/cogs/adventofcode.py +++ /dev/null @@ -1,311 +0,0 @@ -import asyncio -import logging -import re -from datetime import datetime, timedelta - -import aiohttp -import discord -import inflect -import pytz -from bs4 import BeautifulSoup -from discord.ext import commands - -from bot.config import settings - -log = logging.getLogger(__name__) -loop = asyncio.get_event_loop() - -previous_year = 0 if datetime.now(tz=pytz.timezone("EST")).strftime("%m") == "12" else 1 -YEAR = int(datetime.now(tz=pytz.timezone("EST")).strftime("%Y")) - previous_year - -API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/975452.json" -INTERVAL = 120 -AOC_REQUEST_HEADER = {"user-agent": "TWT AoC Event Bot"} -AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} -ENGINE = inflect.engine() - - -def time_left_to_aoc_midnight(): - """Calculates the amount of time left until midnight in UTC-5 (Advent of Code maintainer timezone).""" - # Change all time properties back to 00:00 - todays_midnight = datetime.now(tz=pytz.timezone("EST")).replace(microsecond=0, second=0, minute=0, hour=0) - - # We want tomorrow so add a day on - tomorrow = todays_midnight + timedelta(days=1) - - # Calculate the timedelta between the current time and midnight - return tomorrow, tomorrow - datetime.now(tz=pytz.timezone("UTC")) - - -async def day_countdown(bot: commands.Bot) -> None: - """ - Calculate the number of seconds left until the next day of Advent. - Once we have calculated this we should then sleep that number and when the time is reached, ping - the Advent of Code role notifying them that the new challenge is ready. - """ - while ( - int(datetime.now(tz=pytz.timezone("EST")).day) in range(1, 25) - and int(datetime.now(tz=pytz.timezone("EST")).month) == 12 - ): - tomorrow, time_left = time_left_to_aoc_midnight() - - # Prevent bot from being slightly too early in trying to announce today's puzzle - await asyncio.sleep(time_left.seconds + 1) - - channel = bot.get_channel(settings.aoc.channel_id) - - if not channel: - break - - aoc_role = channel.guild.get_role(settings.aoc.role_id) - if not aoc_role: - break - - puzzle_url = f"https://adventofcode.com/{YEAR}/day/{tomorrow.day}" - - # Check if the puzzle is already available to prevent our members from spamming - # the puzzle page before it's available by making a small HEAD request. - for retry in range(1, 5): - async with bot.session.head(puzzle_url, raise_for_status=False) as resp: - if resp.status == 200: - break - await asyncio.sleep(10) - else: - break - - await channel.send( - f"{aoc_role.mention} Good morning! Day {tomorrow.day} is ready to be attempted. " - f"View it online now at {puzzle_url}. Good luck!", - allowed_mentions=discord.AllowedMentions( - everyone=False, - users=False, - roles=[discord.Object(settings.aoc.role_id)], - ), - ) - - -class Member: - def __init__(self, results): - self.global_score = results["global_score"] - self.name = results["name"] - self.stars = results["stars"] - self.last_star_ts = results["last_star_ts"] - self.completion_day_level = results["completion_day_level"] - self.id = results["id"] - self.local_score = results["local_score"] - - -class AdventOfCode(commands.Cog, name="Advent of Code"): - def __init__(self, bot): - self.bot = bot - - self._base_url = f"https://adventofcode.com/{YEAR}" - self.global_leaderboard_url = f"https://adventofcode.com/{YEAR}/leaderboard" - self.private_leaderboard_url = f"{self._base_url}/leaderboard/private/view/975452" - - countdown_coro = day_countdown(self.bot) - self.countdown_task = loop.create_task(countdown_coro) - - @commands.group(name="adventofcode", aliases=("aoc",)) - async def adventofcode_group(self, ctx: commands.Context) -> None: - """All of the Advent of Code commands.""" - if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) - - @adventofcode_group.command( - name="subscribe", - aliases=("sub", "notifications", "notify", "notifs"), - brief="Notifications for new days", - ) - async def aoc_subscribe(self, ctx: commands.Context) -> None: - """Assign the role for notifications about new days being ready.""" - if ctx.channel.id != settings.aoc.channel_id: - await ctx.send(f"Please use the <#{settings.aoc.channel_id}> channel") - return - - role = ctx.guild.get_role(settings.aoc.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." - ) - - @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days") - async def aoc_unsubscribe(self, ctx: commands.Context) -> None: - """Remove the role for notifications about new days being ready.""" - if ctx.channel.id != settings.aoc.channel_id: - await ctx.send(f"Please use the <#{settings.aoc.channel_id}> channel") - return - - role = ctx.guild.get_role(settings.aoc.role_id) - - 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.") - - @adventofcode_group.command( - name="countdown", - aliases=("count", "c"), - brief="Return time left until Aoc Finishes", - ) - async def aoc_countdown(self, ctx: commands.Context) -> None: - """Return time left until Aoc Finishes.""" - if ctx.channel.id != settings.aoc.channel_id: - await ctx.send(f"Please use the <#{settings.aoc.channel_id}> channel") - return - - if ( - int(datetime.now(tz=pytz.timezone("EST")).day) in range(1, 25) - and int(datetime.now(tz=pytz.timezone("EST")).month) == 12 - ): - days = 24 - int(datetime.now().strftime("%d")) - hours = 23 - int(datetime.now().strftime("%H")) - minutes = 59 - int(datetime.now().strftime("%M")) - - embed = discord.Embed( - title="Advent of Code", - description=f"There are {str(days)} days {str(hours)} hours " - f"and {str(minutes)} minutes left until AOC gets over.", - ) - embed.set_footer(text=ctx.author.display_name, icon_url=ctx.author.display_avatar.url) - await ctx.send(embed=embed) - - else: - await ctx.send("Aoc Hasn't Started Yet!") - - @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)") - async def join_leaderboard(self, ctx: commands.Context) -> None: - """DM the user the information for joining the TWT AoC private leaderboard.""" - if ctx.channel.id != settings.aoc.channel_id: - await ctx.send(f"Please use the <#{settings.aoc.channel_id}> channel") - return - - author = ctx.message.author - - info_str = ( - "Head over to https://adventofcode.com/leaderboard/private " - "with code `975452-d90a48b0` to join the TWT private leaderboard!" - ) - try: - await author.send(info_str) - except discord.errors.Forbidden: - await ctx.send(f":x: {author.mention}, please (temporarily) enable DMs to receive the join code") - else: - await ctx.message.add_reaction("\U0001F4E8") - - @adventofcode_group.command( - name="leaderboard", - aliases=("board", "lb"), - brief="Get a snapshot of the TWT private AoC leaderboard", - ) - async def aoc_leaderboard(self, ctx: commands.Context): - if ctx.channel.id != settings.aoc.channel_id: - return await ctx.send(f"Please use the <#{settings.aoc.channel_id}> channel") - - api_url = API_URL - - async with aiohttp.ClientSession(cookies=AOC_SESSION_COOKIE, headers=AOC_REQUEST_HEADER) as session: - async with session.get(api_url) as resp: - if resp.status == 200: - leaderboard = await resp.json() - else: - resp.raise_for_status() - - members = [Member(leaderboard["members"][id]) for id in leaderboard["members"]] - - embed = discord.Embed( - title=f"{ctx.guild.name} Advent of Code Leaderboard", - colour=discord.Colour(0x68C290), - url=f"https://adventofcode.com/{YEAR}/leaderboard/private/view/975452", - ) - - leaderboard = { - "owner_id": leaderboard["owner_id"], - "event": leaderboard["event"], - "members": members, - } - members = leaderboard["members"] - - for i, member in enumerate(sorted(members, key=lambda x: x.local_score, reverse=True)[:10], 1): - embed.add_field( - name=f"{ENGINE.ordinal(i)} Place: {member.name} ({member.stars} :star:)", - value=f"Local Score: {member.local_score} | Global Score: {member.global_score}", - inline=False, - ) - - tomorrow, _ = time_left_to_aoc_midnight() - embed.set_footer(text=f"Current Day: {tomorrow.day - 1}/25") - - await ctx.send(embed=embed) - - @adventofcode_group.command( - name="global", - aliases=("globalboard", "gb"), - brief="Get a snapshot of the global AoC leaderboard", - ) - async def global_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10): - if ctx.channel.id != settings.aoc.channel_id: - await ctx.send(f"Please use the <#{settings.aoc.channel_id}>") - return - - aoc_url = f"https://adventofcode.com/{YEAR}/leaderboard" - number_of_people_to_display = min(25, number_of_people_to_display) - - async with aiohttp.ClientSession(headers=AOC_REQUEST_HEADER) as session: - async with session.get(aoc_url) as resp: - if resp.status == 200: - raw_html = await resp.text() - else: - resp.raise_for_status() - - soup = BeautifulSoup(raw_html, "html.parser") - ele = soup.find_all("div", class_="leaderboard-entry") - - exp = r"(?:[ ]{,2}(\d+)\))?[ ]+(\d+)\s+([\w\(\)\#\@\-\d ]+)" - - lb_list = [] - for entry in ele: - # Strip off the AoC++ decorator - raw_str = entry.text.replace("(AoC++)", "").rstrip() - - # Group 1: Rank - # Group 2: Global Score - # Group 3: Member string - r = re.match(exp, raw_str) - - rank = int(r.group(1)) if r.group(1) else None - global_score = int(r.group(2)) - - member = r.group(3) - if member.lower().startswith("(anonymous"): - # Normalize anonymous user string by stripping () and title casing - member = re.sub(r"[\(\)]", "", member).title() - - lb_list.append((rank, global_score, member)) - - s_desc = "\n".join( - f"`{index}` {lb_list[index-1][2]} - {lb_list[index-1][1]} " - for index, title in enumerate(lb_list[:number_of_people_to_display], start=1) - ) - - embed = discord.Embed( - title="Advent of Code Global Leaderboard", - colour=discord.Colour(0x68C290), - url="https://adventofcode.com", - description=s_desc, - ) - await ctx.send(embed=embed) - - -async def setup(bot): - await bot.add_cog(AdventOfCode(bot)) diff --git a/bot/extensions/adventofcode/__init__.py b/bot/extensions/adventofcode/__init__.py new file mode 100644 index 00000000..df59c28a --- /dev/null +++ b/bot/extensions/adventofcode/__init__.py @@ -0,0 +1,9 @@ +from bot.core import DiscordBot + +from .commands import AdventOfCode +from .tasks import AdventOfCodeTasks + + +async def setup(bot: DiscordBot) -> None: + await bot.add_cog(AdventOfCode(bot=bot)) + await bot.add_cog(AdventOfCodeTasks(bot=bot)) diff --git a/bot/extensions/adventofcode/commands.py b/bot/extensions/adventofcode/commands.py new file mode 100644 index 00000000..932bb117 --- /dev/null +++ b/bot/extensions/adventofcode/commands.py @@ -0,0 +1,216 @@ +import logging +import re +from datetime import datetime + +import aiohttp +import discord +import pytz +from bs4 import BeautifulSoup +from discord import app_commands +from discord.ext import commands + +from bot import core +from bot.config import settings + +log = logging.getLogger(__name__) + +YEAR = datetime.now(tz=pytz.timezone("EST")).year +API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/975452.json" +INTERVAL = 120 +AOC_REQUEST_HEADER = {"user-agent": "TWT AoC Event Bot"} +AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} + + +def ordinal(n: int): + if 11 <= (n % 100) <= 13: + suffix = "th" + else: + suffix = ["th", "st", "nd", "rd", "th"][min(n % 10, 4)] + return str(n) + suffix + + +class Member: + def __init__(self, results): + self.global_score = results["global_score"] + self.name = results["name"] + self.stars = results["stars"] + self.last_star_ts = results["last_star_ts"] + self.completion_day_level = results["completion_day_level"] + self.id = results["id"] + self.local_score = results["local_score"] + + +class AdventOfCode(commands.GroupCog, group_name="aoc"): + def __init__(self, bot): + self.bot = bot + + @property + def role(self) -> discord.Role: + return self.bot.get_role(settings.aoc.role_id) + + @app_commands.command() + async def subscribe(self, interaction: core.InteractionType) -> None: + """Subscribe to receive notifications for new puzzles""" + + if self.role not in interaction.user.roles: + await interaction.user.add_roles(self.role) + await interaction.response.send_message( + "Okay! You have been __subscribed__ to notifications about new Advent of Code tasks." + "You can run `/aoc unsubscribe` to disable them again for you.", + ephemeral=True, + ) + else: + await interaction.response.send_message( + "Hey, you already are receiving notifications about new Advent of Code tasks." + "If you don't want them any more, run `/aoc unsubscribe` instead.", + ephemeral=True, + ) + + @app_commands.command() + async def unsubscribe(self, interaction: core.InteractionType) -> None: + """Unsubscribe from receiving notifications for new puzzles""" + + if self.role in interaction.user.roles: + await interaction.user.remove_roles(self.role) + await interaction.response.send_message( + "Okay! You have been __unsubscribed__ from notifications about new Advent of Code tasks.", + ephemeral=True, + ) + else: + await interaction.response.send_message( + "Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.", + ephemeral=True, + ) + + @app_commands.command() + async def countdown(self, interaction: core.InteractionType) -> None: + """Get the time left until the next puzzle is released""" + + if ( + int(datetime.now(tz=pytz.timezone("EST")).day) in range(1, 25) + and int(datetime.now(tz=pytz.timezone("EST")).month) == 12 + ): + days = 24 - int(datetime.now().strftime("%d")) + hours = 23 - int(datetime.now().strftime("%H")) + minutes = 59 - int(datetime.now().strftime("%M")) + + embed = discord.Embed( + title="Advent of Code", + description=f"There are {str(days)} days {str(hours)} hours " + f"and {str(minutes)} minutes left until AOC gets over.", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + + else: + await interaction.response.send_message("Advent of Code is not currently running.", ephemeral=True) + + @app_commands.command(name="join") + async def join_leaderboard(self, interaction: core.InteractionType) -> None: + """Learn how to join the leaderboard""" + + await interaction.response.send_message( + "Head over to https://adventofcode.com/leaderboard/private" + "with the code `975452-d90a48b0` to join the TWT private leaderboard!", + ephemeral=True, + ) + + @app_commands.command() + async def leaderboard(self, interaction: core.InteractionType) -> None: + """Get a snapshot of the TWT private AoC leaderboard""" + + if interaction.channel_id != settings.aoc.channel_id: + return await interaction.response.send_message( + f"Please use the <#{settings.aoc.channel_id}> channel", ephemeral=True + ) + + async with aiohttp.ClientSession(cookies=AOC_SESSION_COOKIE, headers=AOC_REQUEST_HEADER) as session: + async with session.get(API_URL) as resp: + if resp.status == 200: + leaderboard = await resp.json() + else: + resp.raise_for_status() + + members = [Member(leaderboard["members"][id]) for id in leaderboard["members"]] + + embed = discord.Embed( + title=f"{interaction.guild.name} Advent of Code Leaderboard", + colour=discord.Colour(0x68C290), + url=f"https://adventofcode.com/{YEAR}/leaderboard/private/view/975452", + ) + + leaderboard = { + "owner_id": leaderboard["owner_id"], + "event": leaderboard["event"], + "members": members, + } + members = leaderboard["members"] + + for i, member in enumerate(sorted(members, key=lambda x: x.local_score, reverse=True)[:10], 1): + embed.add_field( + name=f"{ordinal(i)} Place: {member.name} ({member.stars} ⭐)", + value=f"Local Score: {member.local_score} | Global Score: {member.global_score}", + inline=False, + ) + embed.set_footer(text=f"Current Day: {datetime.now(tz=pytz.timezone('EST')).day}/25") + + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="global") + async def global_leaderboard(self, interaction: core.InteractionType, number_of_people_to_display: int = 10): + """Get a snapshot of the global AoC leaderboard""" + if interaction.channel_id != settings.aoc.channel_id: + return await interaction.response.send_message( + f"Please use the <#{settings.aoc.channel_id}> channel", ephemeral=True + ) + + aoc_url = f"https://adventofcode.com/{YEAR}/leaderboard" + number_of_people_to_display = min(25, number_of_people_to_display) + + async with aiohttp.ClientSession(headers=AOC_REQUEST_HEADER) as session: + async with session.get(aoc_url) as resp: + if resp.status == 200: + raw_html = await resp.text() + else: + resp.raise_for_status() + + soup = BeautifulSoup(raw_html, "html.parser") + ele = soup.find_all("div", class_="leaderboard-entry") + + exp = r"(?:[ ]{,2}(\d+)\))?[ ]+(\d+)\s+([\w\(\)\#\@\-\d ]+)" + + lb_list = [] + for entry in ele: + # Strip off the AoC++ decorator + raw_str = entry.text.replace("(AoC++)", "").rstrip() + + # Group 1: Rank + # Group 2: Global Score + # Group 3: Member string + r = re.match(exp, raw_str) + + rank = int(r.group(1)) if r.group(1) else None + global_score = int(r.group(2)) + + member = r.group(3) + if member.lower().startswith("(anonymous"): + # Normalize anonymous user string by stripping () and title casing + member = re.sub(r"[\(\)]", "", member).title() + + lb_list.append((rank, global_score, member)) + + s_desc = "\n".join( + f"`{index}` {lb_list[index-1][2]} - {lb_list[index-1][1]} " + for index, title in enumerate(lb_list[:number_of_people_to_display], start=1) + ) + + embed = discord.Embed( + title="Advent of Code Global Leaderboard", + colour=discord.Colour(0x68C290), + url="https://adventofcode.com", + description=s_desc, + ) + await interaction.response.send_message(embed=embed) + + +async def setup(bot): + await bot.add_cog(AdventOfCode(bot)) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py new file mode 100644 index 00000000..ab0e394b --- /dev/null +++ b/bot/extensions/adventofcode/tasks.py @@ -0,0 +1,53 @@ +import asyncio +import datetime as dt +from datetime import datetime + +import aiohttp +import discord +import pytz +from discord.ext import commands, tasks + +from bot import core +from bot.config import settings + +aoc_time = dt.time(hour=0, minute=0, second=1, tzinfo=pytz.timezone("EST")) +YEAR = datetime.now(tz=pytz.timezone("EST")).year + + +class AdventOfCodeTasks(commands.Cog): + """Tasks for the Advent of Code cog.""" + + def __init__(self, bot: core.DiscordBot): + self.bot = bot + self.daily_puzzle.start() + + def cog_unload(self) -> None: + self.daily_puzzle.cancel() + + @property + def channel(self) -> discord.TextChannel: + return self.bot.get_channel(settings.aoc.channel_id) + + @tasks.loop(time=aoc_time) + async def daily_puzzle(self) -> None: + """Post the daily Advent of Code puzzle""" + + day = datetime.now(tz=pytz.timezone("EST")).day + month = datetime.now(tz=pytz.timezone("EST")).month + if day > 25 or month != 12: + return + + puzzle_url = f"https://adventofcode.com/{YEAR}/day/{day}" + # Check if the puzzle is already available + for retry in range(4): + async with aiohttp.ClientSession(raise_for_status=False) as session: + async with session.get(puzzle_url) as resp: + if resp.status == 200: + break + await asyncio.sleep(10) + + await self.channel.send( + f"<@&{settings.aoc.role_id}> Good morning! Day {day} is ready to be attempted." + f"View it online now at {puzzle_url}. Good luck!", + allowed_mentions=discord.AllowedMentions(roles=True), + ) diff --git a/cli.py b/cli.py index 7908687f..0677de46 100644 --- a/cli.py +++ b/cli.py @@ -138,6 +138,7 @@ async def main(ctx): "bot.extensions.levelling", "bot.extensions.persistent_roles", "bot.extensions.polls", + "bot.extensions.adventofcode", "bot.cogs._help", "bot.cogs.clashofcode", "bot.cogs.roles", From 13f13dc55c6adea595d565b3bf34553aeaed7869 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:19:04 +0200 Subject: [PATCH 02/18] Did the requested changes --- bot/config.py | 2 + bot/extensions/adventofcode/commands.py | 56 ++++++++++++++----------- bot/extensions/adventofcode/tasks.py | 33 ++++++++++++++- cli.py | 8 ++-- example.env | 2 + poetry.lock | 13 +++++- pyproject.toml | 1 + 7 files changed, 83 insertions(+), 32 deletions(-) diff --git a/bot/config.py b/bot/config.py index f06a98e8..494feeef 100644 --- a/bot/config.py +++ b/bot/config.py @@ -10,6 +10,8 @@ class AoC(BaseModel): channel_id: int role_id: int + leaderboard_id: int + leaderboard_code: str session_cookie: str diff --git a/bot/extensions/adventofcode/commands.py b/bot/extensions/adventofcode/commands.py index 932bb117..4879ab13 100644 --- a/bot/extensions/adventofcode/commands.py +++ b/bot/extensions/adventofcode/commands.py @@ -1,6 +1,6 @@ -import logging import re from datetime import datetime +from typing import Optional import aiohttp import discord @@ -8,16 +8,17 @@ from bs4 import BeautifulSoup from discord import app_commands from discord.ext import commands +from pydantic import BaseModel, validator from bot import core from bot.config import settings -log = logging.getLogger(__name__) - YEAR = datetime.now(tz=pytz.timezone("EST")).year -API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/975452.json" -INTERVAL = 120 -AOC_REQUEST_HEADER = {"user-agent": "TWT AoC Event Bot"} + +LEADERBOARD_ID = settings.aoc.leaderboard_id +LEADERBOARD_CODE = settings.aoc.leaderboard_code +API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" +AOC_REQUEST_HEADER = {"user-agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot"} AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} @@ -29,15 +30,20 @@ def ordinal(n: int): return str(n) + suffix -class Member: - def __init__(self, results): - self.global_score = results["global_score"] - self.name = results["name"] - self.stars = results["stars"] - self.last_star_ts = results["last_star_ts"] - self.completion_day_level = results["completion_day_level"] - self.id = results["id"] - self.local_score = results["local_score"] +class Member(BaseModel): + global_score: int + name: str + stars: int + last_star_ts: int + completion_day_level: dict + id: int + local_score: int + + @validator("name", pre=True) + def set_name_to_anonymous(cls, val: Optional[str]) -> str: + if val is None: + return "Anonymous User" + return val class AdventOfCode(commands.GroupCog, group_name="aoc"): @@ -49,7 +55,7 @@ def role(self) -> discord.Role: return self.bot.get_role(settings.aoc.role_id) @app_commands.command() - async def subscribe(self, interaction: core.InteractionType) -> None: + async def subscribe(self, interaction: core.InteractionType): """Subscribe to receive notifications for new puzzles""" if self.role not in interaction.user.roles: @@ -67,7 +73,7 @@ async def subscribe(self, interaction: core.InteractionType) -> None: ) @app_commands.command() - async def unsubscribe(self, interaction: core.InteractionType) -> None: + async def unsubscribe(self, interaction: core.InteractionType): """Unsubscribe from receiving notifications for new puzzles""" if self.role in interaction.user.roles: @@ -83,7 +89,7 @@ async def unsubscribe(self, interaction: core.InteractionType) -> None: ) @app_commands.command() - async def countdown(self, interaction: core.InteractionType) -> None: + async def countdown(self, interaction: core.InteractionType): """Get the time left until the next puzzle is released""" if ( @@ -105,17 +111,17 @@ async def countdown(self, interaction: core.InteractionType) -> None: await interaction.response.send_message("Advent of Code is not currently running.", ephemeral=True) @app_commands.command(name="join") - async def join_leaderboard(self, interaction: core.InteractionType) -> None: + async def join_leaderboard(self, interaction: core.InteractionType): """Learn how to join the leaderboard""" await interaction.response.send_message( "Head over to https://adventofcode.com/leaderboard/private" - "with the code `975452-d90a48b0` to join the TWT private leaderboard!", + f"with the code `{LEADERBOARD_CODE}` to join the TWT private leaderboard!", ephemeral=True, ) @app_commands.command() - async def leaderboard(self, interaction: core.InteractionType) -> None: + async def leaderboard(self, interaction: core.InteractionType): """Get a snapshot of the TWT private AoC leaderboard""" if interaction.channel_id != settings.aoc.channel_id: @@ -130,12 +136,12 @@ async def leaderboard(self, interaction: core.InteractionType) -> None: else: resp.raise_for_status() - members = [Member(leaderboard["members"][id]) for id in leaderboard["members"]] + members = [Member(**member_data) for member_data in leaderboard["members"].values()] embed = discord.Embed( title=f"{interaction.guild.name} Advent of Code Leaderboard", - colour=discord.Colour(0x68C290), - url=f"https://adventofcode.com/{YEAR}/leaderboard/private/view/975452", + colour=0x68C290, + url=f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}", ) leaderboard = { @@ -205,7 +211,7 @@ async def global_leaderboard(self, interaction: core.InteractionType, number_of_ embed = discord.Embed( title="Advent of Code Global Leaderboard", - colour=discord.Colour(0x68C290), + colour=0x68C290, url="https://adventofcode.com", description=s_desc, ) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index ab0e394b..bf648918 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -1,5 +1,7 @@ import asyncio import datetime as dt +import logging +import traceback from datetime import datetime import aiohttp @@ -9,6 +11,9 @@ from bot import core from bot.config import settings +from bot.services import http, paste + +log = logging.getLogger(__name__) aoc_time = dt.time(hour=0, minute=0, second=1, tzinfo=pytz.timezone("EST")) YEAR = datetime.now(tz=pytz.timezone("EST")).year @@ -21,7 +26,7 @@ def __init__(self, bot: core.DiscordBot): self.bot = bot self.daily_puzzle.start() - def cog_unload(self) -> None: + def cog_unload(self): self.daily_puzzle.cancel() @property @@ -29,7 +34,7 @@ def channel(self) -> discord.TextChannel: return self.bot.get_channel(settings.aoc.channel_id) @tasks.loop(time=aoc_time) - async def daily_puzzle(self) -> None: + async def daily_puzzle(self): """Post the daily Advent of Code puzzle""" day = datetime.now(tz=pytz.timezone("EST")).day @@ -51,3 +56,27 @@ async def daily_puzzle(self) -> None: f"View it online now at {puzzle_url}. Good luck!", allowed_mentions=discord.AllowedMentions(roles=True), ) + + @daily_puzzle.error + async def daily_puzzle_error(self, error: Exception): + """Log any errors raised by the daily puzzle task""" + + content = "\n".join(traceback.format_exception(type(error), error, error.__traceback__)) + header = "Ignored exception in daily puzzle task" + + def wrap(code: str) -> str: + code = code.replace("`", "\u200b`") + return f"```py\n{code}\n```" + + if len(content) > 1024: + document = await paste.create(content) + content = wrap(content[:1024]) + f"\n\n [Full traceback]({document.url})" + else: + content = wrap(content) + + embed = discord.Embed( + title=header, description=content, color=discord.Color.red(), timestamp=discord.utils.utcnow() + ) + await discord.Webhook.from_url(url=settings.errors.webhook_url, session=http.session).send(embed=embed) + + log.error("Daily puzzle task failed", exc_info=error) diff --git a/cli.py b/cli.py index 0677de46..ab56b2fc 100644 --- a/cli.py +++ b/cli.py @@ -130,15 +130,15 @@ async def main(ctx): prefixes = ("t.",) extensions = ( "jishaku", + "bot.extensions.adventofcode", "bot.extensions.challenges", - "bot.extensions.readthedocs", - "bot.extensions.suggestions", "bot.extensions.github", - "bot.extensions.tags", "bot.extensions.levelling", "bot.extensions.persistent_roles", "bot.extensions.polls", - "bot.extensions.adventofcode", + "bot.extensions.readthedocs", + "bot.extensions.suggestions", + "bot.extensions.tags", "bot.cogs._help", "bot.cogs.clashofcode", "bot.cogs.roles", diff --git a/example.env b/example.env index 1429bf2e..296d2baa 100644 --- a/example.env +++ b/example.env @@ -2,6 +2,8 @@ # --- AoC AOC__CHANNEL_ID=0 AOC__ROLE_ID=0 +AOC__LEADERBOARD_ID = 975452 +AOC__LEADERBOARD_CODE = 975452-d90a48b0 AOC__SESSION_COOKIE=... # --- Bots diff --git a/poetry.lock b/poetry.lock index de4938f5..e673dfe8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1073,6 +1073,17 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + [[package]] name = "pyyaml" version = "6.0" @@ -1320,4 +1331,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7ebebbfc6febbd084171f90fe33a92e8a460fc1dfcc3bf5080bc7ca389469eba" +content-hash = "db36dee59de44a2fbc6f503360b58f91c44e53b34bd36a840f7f5a81d2cc7a77" diff --git a/pyproject.toml b/pyproject.toml index 6a5af0bf..1a54eb3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ jishaku = {extras = ["procinfo", "profiling"], version = "^2.5.1"} beautifulsoup4 = "^4.12.2" tabulate = "^0.9.0" pillow = "^10.1.0" +pytz = "^2023.3.post1" [tool.poetry.group.dev.dependencies] isort = "^5.12.0" From 649579702fd9af549f2a3662cb7396e19b28627e Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:47:49 +0200 Subject: [PATCH 03/18] more additions --- bot/extensions/adventofcode/commands.py | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/bot/extensions/adventofcode/commands.py b/bot/extensions/adventofcode/commands.py index 4879ab13..7f849d51 100644 --- a/bot/extensions/adventofcode/commands.py +++ b/bot/extensions/adventofcode/commands.py @@ -1,5 +1,5 @@ import re -from datetime import datetime +from datetime import datetime, timedelta from typing import Optional import aiohttp @@ -61,13 +61,13 @@ async def subscribe(self, interaction: core.InteractionType): if self.role not in interaction.user.roles: await interaction.user.add_roles(self.role) await interaction.response.send_message( - "Okay! You have been __subscribed__ to notifications about new Advent of Code tasks." + "Okay! You have been __subscribed__ to notifications about new Advent of Code puzzles." "You can run `/aoc unsubscribe` to disable them again for you.", ephemeral=True, ) else: await interaction.response.send_message( - "Hey, you already are receiving notifications about new Advent of Code tasks." + "Hey, you already are receiving notifications about new Advent of Code puzzles." "If you don't want them any more, run `/aoc unsubscribe` instead.", ephemeral=True, ) @@ -79,12 +79,12 @@ async def unsubscribe(self, interaction: core.InteractionType): if self.role in interaction.user.roles: await interaction.user.remove_roles(self.role) await interaction.response.send_message( - "Okay! You have been __unsubscribed__ from notifications about new Advent of Code tasks.", + "Okay! You have been __unsubscribed__ from notifications about new Advent of Code puzzles.", ephemeral=True, ) else: await interaction.response.send_message( - "Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.", + "Hey, you don't even get any notifications about new Advent of Code puzzles currently anyway.", ephemeral=True, ) @@ -92,23 +92,23 @@ async def unsubscribe(self, interaction: core.InteractionType): async def countdown(self, interaction: core.InteractionType): """Get the time left until the next puzzle is released""" - if ( - int(datetime.now(tz=pytz.timezone("EST")).day) in range(1, 25) - and int(datetime.now(tz=pytz.timezone("EST")).month) == 12 - ): - days = 24 - int(datetime.now().strftime("%d")) - hours = 23 - int(datetime.now().strftime("%H")) - minutes = 59 - int(datetime.now().strftime("%M")) - - embed = discord.Embed( - title="Advent of Code", - description=f"There are {str(days)} days {str(hours)} hours " - f"and {str(minutes)} minutes left until AOC gets over.", + now = datetime.now(tz=pytz.timezone("EST")) + if now.month == 12: + # If it's December, calculate time until the next midnight EST time + target = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + else: + # If it's not December, calculate time until the first of December + target = datetime(now.year, 12, 1, tzinfo=pytz.timezone("EST")) + + if target < now: + return await interaction.response.send_message( + "Advent of Code is over for this year! See you next year!", ephemeral=True ) - await interaction.response.send_message(embed=embed, ephemeral=True) - else: - await interaction.response.send_message("Advent of Code is not currently running.", ephemeral=True) + await interaction.response.send_message( + f"There are {discord.utils.format_dt(target, 'R')} left until the next puzzle is released!", + ephemeral=True, + ) @app_commands.command(name="join") async def join_leaderboard(self, interaction: core.InteractionType): From b2cf6cfd751cf4c383f0b440a9b596818e560365 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:23:29 +0200 Subject: [PATCH 04/18] Added embed that contains information on the daily puzzle --- bot/extensions/adventofcode/tasks.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index bf648918..7e3e20f5 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -7,6 +7,7 @@ import aiohttp import discord import pytz +from bs4 import BeautifulSoup from discord.ext import commands, tasks from bot import core @@ -43,17 +44,38 @@ async def daily_puzzle(self): return puzzle_url = f"https://adventofcode.com/{YEAR}/day/{day}" - # Check if the puzzle is already available + raw_html = None for retry in range(4): async with aiohttp.ClientSession(raise_for_status=False) as session: async with session.get(puzzle_url) as resp: if resp.status == 200: + raw_html = await resp.text() break await asyncio.sleep(10) + if not raw_html: + return await self.channel.send( + f"<@&{settings.aoc.role_id}> Good morning! Day {day} is ready to be attempted." + f"View it online now at {puzzle_url}. Good luck!", + allowed_mentions=discord.AllowedMentions(roles=True), + ) + + soup = BeautifulSoup(raw_html, "html.parser") + article = soup.find("article", class_="day-desc") + title = article.find("h2").text.strip().replace("---", "") + desc = article.find("p").text.strip() + + embed = discord.Embed( + title=title, + description=desc, + color=discord.Color.red(), + url=puzzle_url, + timestamp=datetime.now(tz=pytz.timezone("EST")).replace(hour=0, minute=0, second=0, microsecond=0), + ) + await self.channel.send( - f"<@&{settings.aoc.role_id}> Good morning! Day {day} is ready to be attempted." - f"View it online now at {puzzle_url}. Good luck!", + f"<@&{settings.aoc.role_id}> Good morning! Day {day} is ready to be attempted.", + embed=embed, allowed_mentions=discord.AllowedMentions(roles=True), ) From ddee214166e0b63121332467d0c28c1f71f18e09 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:38:41 +0200 Subject: [PATCH 05/18] embed author --- bot/extensions/adventofcode/tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index 7e3e20f5..e1a3b123 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -62,7 +62,7 @@ async def daily_puzzle(self): soup = BeautifulSoup(raw_html, "html.parser") article = soup.find("article", class_="day-desc") - title = article.find("h2").text.strip().replace("---", "") + title = article.find("h2").text.replace("---", "").strip() desc = article.find("p").text.strip() embed = discord.Embed( @@ -72,6 +72,9 @@ async def daily_puzzle(self): url=puzzle_url, timestamp=datetime.now(tz=pytz.timezone("EST")).replace(hour=0, minute=0, second=0, microsecond=0), ) + embed.set_author( + name="Advent Of Code", url="https://adventofcode.com", icon_url="https://adventofcode.com/favicon.png" + ) await self.channel.send( f"<@&{settings.aoc.role_id}> Good morning! Day {day} is ready to be attempted.", From 9a3e91e43353afe5ee25a35cefcbfa3ab67b236b Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Thu, 9 Nov 2023 21:42:22 +0200 Subject: [PATCH 06/18] Did the suggested changes --- bot/extensions/adventofcode/commands.py | 222 +----------------------- bot/extensions/adventofcode/utils.py | 95 ++++++++++ bot/extensions/adventofcode/views.py | 119 +++++++++++++ 3 files changed, 223 insertions(+), 213 deletions(-) create mode 100644 bot/extensions/adventofcode/utils.py create mode 100644 bot/extensions/adventofcode/views.py diff --git a/bot/extensions/adventofcode/commands.py b/bot/extensions/adventofcode/commands.py index 7f849d51..927becc6 100644 --- a/bot/extensions/adventofcode/commands.py +++ b/bot/extensions/adventofcode/commands.py @@ -1,222 +1,18 @@ -import re -from datetime import datetime, timedelta -from typing import Optional - -import aiohttp -import discord -import pytz -from bs4 import BeautifulSoup from discord import app_commands from discord.ext import commands -from pydantic import BaseModel, validator from bot import core -from bot.config import settings - -YEAR = datetime.now(tz=pytz.timezone("EST")).year - -LEADERBOARD_ID = settings.aoc.leaderboard_id -LEADERBOARD_CODE = settings.aoc.leaderboard_code -API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" -AOC_REQUEST_HEADER = {"user-agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot"} -AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} - - -def ordinal(n: int): - if 11 <= (n % 100) <= 13: - suffix = "th" - else: - suffix = ["th", "st", "nd", "rd", "th"][min(n % 10, 4)] - return str(n) + suffix +from bot.extensions.adventofcode.utils import home_embed +from bot.extensions.adventofcode.views import CreateAdventOfCodeView -class Member(BaseModel): - global_score: int - name: str - stars: int - last_star_ts: int - completion_day_level: dict - id: int - local_score: int - - @validator("name", pre=True) - def set_name_to_anonymous(cls, val: Optional[str]) -> str: - if val is None: - return "Anonymous User" - return val - - -class AdventOfCode(commands.GroupCog, group_name="aoc"): +class AdventOfCode(commands.Cog): def __init__(self, bot): self.bot = bot + self._create_aoc_view = CreateAdventOfCodeView(timeout=None) + self.bot.add_view(self._create_aoc_view) - @property - def role(self) -> discord.Role: - return self.bot.get_role(settings.aoc.role_id) - - @app_commands.command() - async def subscribe(self, interaction: core.InteractionType): - """Subscribe to receive notifications for new puzzles""" - - if self.role not in interaction.user.roles: - await interaction.user.add_roles(self.role) - await interaction.response.send_message( - "Okay! You have been __subscribed__ to notifications about new Advent of Code puzzles." - "You can run `/aoc unsubscribe` to disable them again for you.", - ephemeral=True, - ) - else: - await interaction.response.send_message( - "Hey, you already are receiving notifications about new Advent of Code puzzles." - "If you don't want them any more, run `/aoc unsubscribe` instead.", - ephemeral=True, - ) - - @app_commands.command() - async def unsubscribe(self, interaction: core.InteractionType): - """Unsubscribe from receiving notifications for new puzzles""" - - if self.role in interaction.user.roles: - await interaction.user.remove_roles(self.role) - await interaction.response.send_message( - "Okay! You have been __unsubscribed__ from notifications about new Advent of Code puzzles.", - ephemeral=True, - ) - else: - await interaction.response.send_message( - "Hey, you don't even get any notifications about new Advent of Code puzzles currently anyway.", - ephemeral=True, - ) - - @app_commands.command() - async def countdown(self, interaction: core.InteractionType): - """Get the time left until the next puzzle is released""" - - now = datetime.now(tz=pytz.timezone("EST")) - if now.month == 12: - # If it's December, calculate time until the next midnight EST time - target = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) - else: - # If it's not December, calculate time until the first of December - target = datetime(now.year, 12, 1, tzinfo=pytz.timezone("EST")) - - if target < now: - return await interaction.response.send_message( - "Advent of Code is over for this year! See you next year!", ephemeral=True - ) - - await interaction.response.send_message( - f"There are {discord.utils.format_dt(target, 'R')} left until the next puzzle is released!", - ephemeral=True, - ) - - @app_commands.command(name="join") - async def join_leaderboard(self, interaction: core.InteractionType): - """Learn how to join the leaderboard""" - - await interaction.response.send_message( - "Head over to https://adventofcode.com/leaderboard/private" - f"with the code `{LEADERBOARD_CODE}` to join the TWT private leaderboard!", - ephemeral=True, - ) - - @app_commands.command() - async def leaderboard(self, interaction: core.InteractionType): - """Get a snapshot of the TWT private AoC leaderboard""" - - if interaction.channel_id != settings.aoc.channel_id: - return await interaction.response.send_message( - f"Please use the <#{settings.aoc.channel_id}> channel", ephemeral=True - ) - - async with aiohttp.ClientSession(cookies=AOC_SESSION_COOKIE, headers=AOC_REQUEST_HEADER) as session: - async with session.get(API_URL) as resp: - if resp.status == 200: - leaderboard = await resp.json() - else: - resp.raise_for_status() - - members = [Member(**member_data) for member_data in leaderboard["members"].values()] - - embed = discord.Embed( - title=f"{interaction.guild.name} Advent of Code Leaderboard", - colour=0x68C290, - url=f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}", - ) - - leaderboard = { - "owner_id": leaderboard["owner_id"], - "event": leaderboard["event"], - "members": members, - } - members = leaderboard["members"] - - for i, member in enumerate(sorted(members, key=lambda x: x.local_score, reverse=True)[:10], 1): - embed.add_field( - name=f"{ordinal(i)} Place: {member.name} ({member.stars} ⭐)", - value=f"Local Score: {member.local_score} | Global Score: {member.global_score}", - inline=False, - ) - embed.set_footer(text=f"Current Day: {datetime.now(tz=pytz.timezone('EST')).day}/25") - - await interaction.response.send_message(embed=embed) - - @app_commands.command(name="global") - async def global_leaderboard(self, interaction: core.InteractionType, number_of_people_to_display: int = 10): - """Get a snapshot of the global AoC leaderboard""" - if interaction.channel_id != settings.aoc.channel_id: - return await interaction.response.send_message( - f"Please use the <#{settings.aoc.channel_id}> channel", ephemeral=True - ) - - aoc_url = f"https://adventofcode.com/{YEAR}/leaderboard" - number_of_people_to_display = min(25, number_of_people_to_display) - - async with aiohttp.ClientSession(headers=AOC_REQUEST_HEADER) as session: - async with session.get(aoc_url) as resp: - if resp.status == 200: - raw_html = await resp.text() - else: - resp.raise_for_status() - - soup = BeautifulSoup(raw_html, "html.parser") - ele = soup.find_all("div", class_="leaderboard-entry") - - exp = r"(?:[ ]{,2}(\d+)\))?[ ]+(\d+)\s+([\w\(\)\#\@\-\d ]+)" - - lb_list = [] - for entry in ele: - # Strip off the AoC++ decorator - raw_str = entry.text.replace("(AoC++)", "").rstrip() - - # Group 1: Rank - # Group 2: Global Score - # Group 3: Member string - r = re.match(exp, raw_str) - - rank = int(r.group(1)) if r.group(1) else None - global_score = int(r.group(2)) - - member = r.group(3) - if member.lower().startswith("(anonymous"): - # Normalize anonymous user string by stripping () and title casing - member = re.sub(r"[\(\)]", "", member).title() - - lb_list.append((rank, global_score, member)) - - s_desc = "\n".join( - f"`{index}` {lb_list[index-1][2]} - {lb_list[index-1][1]} " - for index, title in enumerate(lb_list[:number_of_people_to_display], start=1) - ) - - embed = discord.Embed( - title="Advent of Code Global Leaderboard", - colour=0x68C290, - url="https://adventofcode.com", - description=s_desc, - ) - await interaction.response.send_message(embed=embed) - - -async def setup(bot): - await bot.add_cog(AdventOfCode(bot)) + @app_commands.command(name="advent-of-code") + async def advent_of_code(self, interaction: core.InteractionType): + """Returns information about the Advent of Code""" + await interaction.response.send_message(embed=home_embed(), ephemeral=True, view=self._create_aoc_view) diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py new file mode 100644 index 00000000..53aab4de --- /dev/null +++ b/bot/extensions/adventofcode/utils.py @@ -0,0 +1,95 @@ +from datetime import datetime, timedelta +from typing import Optional + +import aiohttp +import discord +import pytz +from pydantic import BaseModel, validator + +from bot.config import settings + +YEAR = datetime.now(tz=pytz.timezone("EST")).year +LEADERBOARD_ID = settings.aoc.leaderboard_id +LEADERBOARD_CODE = settings.aoc.leaderboard_code +API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" +AOC_REQUEST_HEADER = {"user-agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot"} +AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} + + +def home_embed(): + embed = discord.Embed(title="", colour=0x68C290) + embed.set_author( + name="Advent of Code", + url="https://adventofcode.com", + icon_url="https://adventofcode.com/favicon.png", + ) + embed.add_field( + name="Join the Leaderboard!", + value="Head over to https://adventofcode.com/leaderboard/private" + f" with the code `{LEADERBOARD_CODE}` to join the TWT private leaderboard!", + inline=False, + ) + + puzzle_time = next_puzzle() + if puzzle_time: + embed.add_field( + name="Next Puzzle", + value=f"The next puzzle is {discord.utils.format_dt(puzzle_time, 'R')}", + inline=False, + ) + + return embed + + +def ordinal(n: int): + if 11 <= (n % 100) <= 13: + suffix = "th" + else: + suffix = ["th", "st", "nd", "rd", "th"][min(n % 10, 4)] + return str(n) + suffix + + +def next_puzzle(): + now = datetime.now(tz=pytz.timezone("EST")) + if now.month == 12: + if now.day >= 25: + return False + + # If it's December, calculate time until the next midnight EST time + target = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + else: + # If it's not December, calculate time until the first of December + target = datetime(now.year, 12, 1, tzinfo=pytz.timezone("EST")) + + return target + + +class Member(BaseModel): + global_score: int + name: str + stars: int + last_star_ts: int + completion_day_level: dict + id: int + local_score: int + + @validator("name", pre=True) + def set_name_to_anonymous(cls, val: Optional[str]) -> str: + if val is None: + return "Anonymous User" + return val + + +async def fetch_leaderboard(local: bool = False) -> str: + url = f"https://adventofcode.com/{YEAR}/leaderboard" + if local: + url += f"/private/view/{LEADERBOARD_ID}.json" + + async with aiohttp.ClientSession(cookies=AOC_SESSION_COOKIE, headers=AOC_REQUEST_HEADER) as session: + async with session.get(url) as resp: + if resp.status == 200: + response = await resp.text() + else: + resp.raise_for_status() + + return response diff --git a/bot/extensions/adventofcode/views.py b/bot/extensions/adventofcode/views.py new file mode 100644 index 00000000..183c44d9 --- /dev/null +++ b/bot/extensions/adventofcode/views.py @@ -0,0 +1,119 @@ +import json +import re +from datetime import datetime + +import discord +import pytz +from bs4 import BeautifulSoup +from discord import ui + +from bot import core +from bot.extensions.adventofcode.utils import LEADERBOARD_ID, YEAR, Member, fetch_leaderboard, home_embed, ordinal + + +class CreateAdventOfCodeView(ui.View): + HOME_CUSTOM_ID = "extensions:adventofcode:home" + LOCAL_LEADERBOARD_CUSTOM_ID = "extensions:adventofcode:local" + GLOBAL_LEADERBOARD_CUSTOM_ID = "extensions:adventofcode:global" + + def reset_buttons(self): + self.children[0].disabled = True + self.children[1].disabled = False + self.children[2].disabled = False + + @discord.ui.button(label="Home", style=discord.ButtonStyle.gray, emoji="🏠", custom_id=HOME_CUSTOM_ID, disabled=True) + async def home(self, interaction: core.InteractionType, _button: ui.Button): + self.reset_buttons() + await interaction.response.edit_message(embed=home_embed(), view=self) + + @discord.ui.button( + label="Local Leaderboard", style=discord.ButtonStyle.gray, emoji="👥", custom_id=LOCAL_LEADERBOARD_CUSTOM_ID + ) + async def local_leaderboard(self, interaction: core.InteractionType, button: ui.Button): + leaderboard = await fetch_leaderboard(local=True) + leaderboard = json.loads(leaderboard) + + members = [Member(**member_data) for member_data in leaderboard["members"].values()] + + embed = discord.Embed( + title=f"{interaction.guild.name} Advent of Code Leaderboard", + colour=0x68C290, + url=f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}", + ) + + leaderboard = { + "owner_id": leaderboard["owner_id"], + "event": leaderboard["event"], + "members": members, + } + members = leaderboard["members"] + + for i, member in enumerate(sorted(members, key=lambda x: x.local_score, reverse=True)[:10], 1): + embed.add_field( + name=f"{ordinal(i)} Place: {member.name} ({member.stars} ⭐)", + value=f"Local Score: {member.local_score} | Global Score: {member.global_score}", + inline=False, + ) + embed.set_footer(text=f"Current Day: {datetime.now(tz=pytz.timezone('EST')).day}/25") + + button.disabled = True + self.children[0].disabled = False + self.children[2].disabled = False + + await interaction.response.edit_message(embed=embed, view=self) + self.reset_buttons() + + @discord.ui.button( + label="Global Leaderboard", style=discord.ButtonStyle.gray, emoji="🌎", custom_id=GLOBAL_LEADERBOARD_CUSTOM_ID + ) + async def global_leaderboard(self, interaction: core.InteractionType, button: ui.Button): + raw_html = await fetch_leaderboard() + soup = BeautifulSoup(raw_html, "html.parser") + + button.disabled = True + self.children[0].disabled = False + self.children[1].disabled = False + + embed = discord.Embed( + title="Advent of Code Global Leaderboard", + colour=0x68C290, + url="https://adventofcode.com", + ) + + if soup.find("p").text == "Nothing to show on the leaderboard... yet.": + embed.description = "The global leaderboard is not available yet. Please try again later." + await interaction.response.edit_message(embed=embed, view=self) + return self.reset_buttons() + + ele = soup.find_all("div", class_="leaderboard-entry") + exp = r"(?:[ ]{,2}(\d+)\))?[ ]+(\d+)\s+([\w\(\)\#\@\-\d ]+)" + + lb_list = [] + for entry in ele: + # Strip off the AoC++ decorator + raw_str = entry.text.replace("(AoC++)", "").rstrip() + + # Group 1: Rank + # Group 2: Global Score + # Group 3: Member string + r = re.match(exp, raw_str) + + rank = int(r.group(1)) if r.group(1) else None + global_score = int(r.group(2)) + + member = r.group(3) + if member.lower().startswith("(anonymous"): + # Normalize anonymous user string by stripping () and title casing + member = re.sub(r"[\(\)]", "", member).title() + + lb_list.append((rank, global_score, member)) + + s_desc = "\n".join( + f"`{index}` {lb_list[index - 1][2]} - {lb_list[index - 1][1]} " + for index, title in enumerate(lb_list[:10], start=1) + ) + + embed.description = s_desc + + await interaction.response.edit_message(embed=embed, view=self) + self.reset_buttons() From 7a01cd546b5db9494f55f90994edb11062bd024c Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Fri, 17 Nov 2023 23:01:02 +0200 Subject: [PATCH 07/18] updated daily_puzzle error handler to use the new on_error function --- bot/extensions/adventofcode/tasks.py | 40 +++++++--------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index e1a3b123..b79b4ed1 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -1,10 +1,8 @@ import asyncio import datetime as dt import logging -import traceback from datetime import datetime -import aiohttp import discord import pytz from bs4 import BeautifulSoup @@ -12,7 +10,7 @@ from bot import core from bot.config import settings -from bot.services import http, paste +from bot.services import http log = logging.getLogger(__name__) @@ -46,12 +44,11 @@ async def daily_puzzle(self): puzzle_url = f"https://adventofcode.com/{YEAR}/day/{day}" raw_html = None for retry in range(4): - async with aiohttp.ClientSession(raise_for_status=False) as session: - async with session.get(puzzle_url) as resp: - if resp.status == 200: - raw_html = await resp.text() - break - await asyncio.sleep(10) + async with http.session.get(puzzle_url, raise_for_status=False) as resp: + if resp.status == 200: + raw_html = await resp.text() + break + await asyncio.sleep(10) if not raw_html: return await self.channel.send( @@ -83,25 +80,6 @@ async def daily_puzzle(self): ) @daily_puzzle.error - async def daily_puzzle_error(self, error: Exception): - """Log any errors raised by the daily puzzle task""" - - content = "\n".join(traceback.format_exception(type(error), error, error.__traceback__)) - header = "Ignored exception in daily puzzle task" - - def wrap(code: str) -> str: - code = code.replace("`", "\u200b`") - return f"```py\n{code}\n```" - - if len(content) > 1024: - document = await paste.create(content) - content = wrap(content[:1024]) + f"\n\n [Full traceback]({document.url})" - else: - content = wrap(content) - - embed = discord.Embed( - title=header, description=content, color=discord.Color.red(), timestamp=discord.utils.utcnow() - ) - await discord.Webhook.from_url(url=settings.errors.webhook_url, session=http.session).send(embed=embed) - - log.error("Daily puzzle task failed", exc_info=error) + async def daily_puzzle_error(self, _error: Exception): + """Logs any errors that occur during the daily puzzle task""" + await self.bot.on_error("daily_puzzle") From 02ca21bc14a95890f85b2c3802b9803b5f6f512e Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Fri, 17 Nov 2023 23:14:41 +0200 Subject: [PATCH 08/18] replaced pytz in favour of zoneinfo --- bot/extensions/adventofcode/tasks.py | 12 ++++++------ bot/extensions/adventofcode/utils.py | 8 ++++---- poetry.lock | 15 ++------------- pyproject.toml | 1 - 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index b79b4ed1..3f4e5d5d 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -2,9 +2,9 @@ import datetime as dt import logging from datetime import datetime +from zoneinfo import ZoneInfo import discord -import pytz from bs4 import BeautifulSoup from discord.ext import commands, tasks @@ -14,8 +14,8 @@ log = logging.getLogger(__name__) -aoc_time = dt.time(hour=0, minute=0, second=1, tzinfo=pytz.timezone("EST")) -YEAR = datetime.now(tz=pytz.timezone("EST")).year +aoc_time = dt.time(hour=0, minute=0, second=1, tzinfo=ZoneInfo("EST")) +YEAR = datetime.now(tz=ZoneInfo("EST")).year class AdventOfCodeTasks(commands.Cog): @@ -36,8 +36,8 @@ def channel(self) -> discord.TextChannel: async def daily_puzzle(self): """Post the daily Advent of Code puzzle""" - day = datetime.now(tz=pytz.timezone("EST")).day - month = datetime.now(tz=pytz.timezone("EST")).month + day = datetime.now(tz=ZoneInfo("EST")).day + month = datetime.now(tz=ZoneInfo("EST")).month if day > 25 or month != 12: return @@ -67,7 +67,7 @@ async def daily_puzzle(self): description=desc, color=discord.Color.red(), url=puzzle_url, - timestamp=datetime.now(tz=pytz.timezone("EST")).replace(hour=0, minute=0, second=0, microsecond=0), + timestamp=datetime.now(tz=ZoneInfo("EST")).replace(hour=0, minute=0, second=0, microsecond=0), ) embed.set_author( name="Advent Of Code", url="https://adventofcode.com", icon_url="https://adventofcode.com/favicon.png" diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py index 53aab4de..ac9c8fe9 100644 --- a/bot/extensions/adventofcode/utils.py +++ b/bot/extensions/adventofcode/utils.py @@ -1,14 +1,14 @@ from datetime import datetime, timedelta from typing import Optional +from zoneinfo import ZoneInfo import aiohttp import discord -import pytz from pydantic import BaseModel, validator from bot.config import settings -YEAR = datetime.now(tz=pytz.timezone("EST")).year +YEAR = datetime.now(tz=ZoneInfo("EST")).year LEADERBOARD_ID = settings.aoc.leaderboard_id LEADERBOARD_CODE = settings.aoc.leaderboard_code API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" @@ -50,7 +50,7 @@ def ordinal(n: int): def next_puzzle(): - now = datetime.now(tz=pytz.timezone("EST")) + now = datetime.now(tz=ZoneInfo("EST")) if now.month == 12: if now.day >= 25: return False @@ -59,7 +59,7 @@ def next_puzzle(): target = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) else: # If it's not December, calculate time until the first of December - target = datetime(now.year, 12, 1, tzinfo=pytz.timezone("EST")) + target = datetime(now.year, 12, 1, tzinfo=ZoneInfo("EST")) return target diff --git a/poetry.lock b/poetry.lock index 1e04ee5a..7dc6674c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1073,17 +1073,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "pytz" -version = "2023.3.post1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, -] - [[package]] name = "pyyaml" version = "6.0" @@ -1331,4 +1320,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "db36dee59de44a2fbc6f503360b58f91c44e53b34bd36a840f7f5a81d2cc7a77" +content-hash = "7ebebbfc6febbd084171f90fe33a92e8a460fc1dfcc3bf5080bc7ca389469eba" diff --git a/pyproject.toml b/pyproject.toml index 1a54eb3d..6a5af0bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ jishaku = {extras = ["procinfo", "profiling"], version = "^2.5.1"} beautifulsoup4 = "^4.12.2" tabulate = "^0.9.0" pillow = "^10.1.0" -pytz = "^2023.3.post1" [tool.poetry.group.dev.dependencies] isort = "^5.12.0" From a426f7f1044895dcf21240011f90649e6aa29651 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:29:28 +0200 Subject: [PATCH 09/18] fixed bug and upated code to use http.session --- bot/extensions/adventofcode/utils.py | 15 +++++++-------- bot/extensions/adventofcode/views.py | 4 ++-- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py index ac9c8fe9..6c0e9284 100644 --- a/bot/extensions/adventofcode/utils.py +++ b/bot/extensions/adventofcode/utils.py @@ -2,17 +2,17 @@ from typing import Optional from zoneinfo import ZoneInfo -import aiohttp import discord from pydantic import BaseModel, validator from bot.config import settings +from bot.services import http YEAR = datetime.now(tz=ZoneInfo("EST")).year LEADERBOARD_ID = settings.aoc.leaderboard_id LEADERBOARD_CODE = settings.aoc.leaderboard_code API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" -AOC_REQUEST_HEADER = {"user-agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot"} +AOC_REQUEST_HEADER = {"User-Agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot"} AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} @@ -85,11 +85,10 @@ async def fetch_leaderboard(local: bool = False) -> str: if local: url += f"/private/view/{LEADERBOARD_ID}.json" - async with aiohttp.ClientSession(cookies=AOC_SESSION_COOKIE, headers=AOC_REQUEST_HEADER) as session: - async with session.get(url) as resp: - if resp.status == 200: - response = await resp.text() - else: - resp.raise_for_status() + http.session.cookie_jar.update_cookies(AOC_SESSION_COOKIE) + async with http.session.get(url, headers=AOC_REQUEST_HEADER, raise_for_status=True) as resp: + if resp.status == 200: + response = await resp.text() + http.session.cookie_jar.clear() return response diff --git a/bot/extensions/adventofcode/views.py b/bot/extensions/adventofcode/views.py index 183c44d9..0b6a028e 100644 --- a/bot/extensions/adventofcode/views.py +++ b/bot/extensions/adventofcode/views.py @@ -1,9 +1,9 @@ import json import re from datetime import datetime +from zoneinfo import ZoneInfo import discord -import pytz from bs4 import BeautifulSoup from discord import ui @@ -54,7 +54,7 @@ async def local_leaderboard(self, interaction: core.InteractionType, button: ui. value=f"Local Score: {member.local_score} | Global Score: {member.global_score}", inline=False, ) - embed.set_footer(text=f"Current Day: {datetime.now(tz=pytz.timezone('EST')).day}/25") + embed.set_footer(text=f"Current Day: {datetime.now(tz=ZoneInfo('EST')).day}/25") button.disabled = True self.children[0].disabled = False diff --git a/poetry.lock b/poetry.lock index 7dc6674c..a7b6ac39 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1196,6 +1196,17 @@ files = [ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "virtualenv" version = "20.23.0" @@ -1320,4 +1331,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7ebebbfc6febbd084171f90fe33a92e8a460fc1dfcc3bf5080bc7ca389469eba" +content-hash = "0faf30752db0cb31ba0d77ff3b1ede7423c660351dc1f9d036870cd7ec86ef5e" diff --git a/pyproject.toml b/pyproject.toml index 6a5af0bf..c7986713 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ jishaku = {extras = ["procinfo", "profiling"], version = "^2.5.1"} beautifulsoup4 = "^4.12.2" tabulate = "^0.9.0" pillow = "^10.1.0" +tzdata = "^2023.3" [tool.poetry.group.dev.dependencies] isort = "^5.12.0" From 5223fdc9bd56b90b3798dbfda573e4368c14a3c1 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:10:58 +0200 Subject: [PATCH 10/18] set cookie within header --- bot/extensions/adventofcode/utils.py | 19 +++++++++++-------- bot/extensions/adventofcode/views.py | 2 -- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py index 6c0e9284..cfdbd8d7 100644 --- a/bot/extensions/adventofcode/utils.py +++ b/bot/extensions/adventofcode/utils.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import Optional +from typing import Optional, Union from zoneinfo import ZoneInfo import discord @@ -12,8 +12,10 @@ LEADERBOARD_ID = settings.aoc.leaderboard_id LEADERBOARD_CODE = settings.aoc.leaderboard_code API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" -AOC_REQUEST_HEADER = {"User-Agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot"} -AOC_SESSION_COOKIE = {"session": settings.aoc.session_cookie} +AOC_REQUEST_HEADERS = { + "User-Agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot", + "Cookie": f"session={settings.aoc.session_cookie}", +} def home_embed(): @@ -80,15 +82,16 @@ def set_name_to_anonymous(cls, val: Optional[str]) -> str: return val -async def fetch_leaderboard(local: bool = False) -> str: +async def fetch_leaderboard(local: bool = False) -> Union[str, dict]: url = f"https://adventofcode.com/{YEAR}/leaderboard" if local: url += f"/private/view/{LEADERBOARD_ID}.json" - http.session.cookie_jar.update_cookies(AOC_SESSION_COOKIE) - async with http.session.get(url, headers=AOC_REQUEST_HEADER, raise_for_status=True) as resp: + async with http.session.get(url, headers=AOC_REQUEST_HEADERS, raise_for_status=True) as resp: if resp.status == 200: - response = await resp.text() - http.session.cookie_jar.clear() + if local: + response = await resp.json() + else: + response = await resp.text() return response diff --git a/bot/extensions/adventofcode/views.py b/bot/extensions/adventofcode/views.py index 0b6a028e..da5f16c4 100644 --- a/bot/extensions/adventofcode/views.py +++ b/bot/extensions/adventofcode/views.py @@ -1,4 +1,3 @@ -import json import re from datetime import datetime from zoneinfo import ZoneInfo @@ -31,7 +30,6 @@ async def home(self, interaction: core.InteractionType, _button: ui.Button): ) async def local_leaderboard(self, interaction: core.InteractionType, button: ui.Button): leaderboard = await fetch_leaderboard(local=True) - leaderboard = json.loads(leaderboard) members = [Member(**member_data) for member_data in leaderboard["members"].values()] From 1cac2b227b70fc5d61867c1b1a5bf7e45985ac41 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:23:13 +0200 Subject: [PATCH 11/18] code cleanup --- bot/config.py | 2 -- bot/extensions/adventofcode/tasks.py | 13 +++++-------- bot/extensions/adventofcode/utils.py | 3 +-- bot/extensions/adventofcode/views.py | 2 +- example.env | 1 - 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/bot/config.py b/bot/config.py index cb980e2a..8af5d29e 100644 --- a/bot/config.py +++ b/bot/config.py @@ -10,7 +10,6 @@ class AoC(BaseModel): channel_id: int role_id: int - leaderboard_id: int leaderboard_code: str session_cookie: str @@ -99,7 +98,6 @@ class CustomRoles(BaseModel): class YouTube(BaseModel): channel_id: str - text_channel_id: int role_id: int diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index 3f4e5d5d..d2bbef30 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -1,6 +1,5 @@ import asyncio import datetime as dt -import logging from datetime import datetime from zoneinfo import ZoneInfo @@ -10,12 +9,10 @@ from bot import core from bot.config import settings +from bot.extensions.adventofcode.utils import YEAR from bot.services import http -log = logging.getLogger(__name__) - aoc_time = dt.time(hour=0, minute=0, second=1, tzinfo=ZoneInfo("EST")) -YEAR = datetime.now(tz=ZoneInfo("EST")).year class AdventOfCodeTasks(commands.Cog): @@ -36,9 +33,9 @@ def channel(self) -> discord.TextChannel: async def daily_puzzle(self): """Post the daily Advent of Code puzzle""" - day = datetime.now(tz=ZoneInfo("EST")).day - month = datetime.now(tz=ZoneInfo("EST")).month - if day > 25 or month != 12: + now = datetime.now(tz=ZoneInfo("EST")) + day = now.day + if day > 25 or now.month != 12: return puzzle_url = f"https://adventofcode.com/{YEAR}/day/{day}" @@ -67,7 +64,7 @@ async def daily_puzzle(self): description=desc, color=discord.Color.red(), url=puzzle_url, - timestamp=datetime.now(tz=ZoneInfo("EST")).replace(hour=0, minute=0, second=0, microsecond=0), + timestamp=now.replace(hour=0, minute=0, second=0, microsecond=0), ) embed.set_author( name="Advent Of Code", url="https://adventofcode.com", icon_url="https://adventofcode.com/favicon.png" diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py index cfdbd8d7..554d7725 100644 --- a/bot/extensions/adventofcode/utils.py +++ b/bot/extensions/adventofcode/utils.py @@ -9,9 +9,8 @@ from bot.services import http YEAR = datetime.now(tz=ZoneInfo("EST")).year -LEADERBOARD_ID = settings.aoc.leaderboard_id LEADERBOARD_CODE = settings.aoc.leaderboard_code -API_URL = f"https://adventofcode.com/{YEAR}/leaderboard/private/view/{LEADERBOARD_ID}.json" +LEADERBOARD_ID = LEADERBOARD_CODE.split("-")[0] AOC_REQUEST_HEADERS = { "User-Agent": "Tech With Tim Discord Bot https://github.com/SylteA/Discord-Bot", "Cookie": f"session={settings.aoc.session_cookie}", diff --git a/bot/extensions/adventofcode/views.py b/bot/extensions/adventofcode/views.py index da5f16c4..6312a414 100644 --- a/bot/extensions/adventofcode/views.py +++ b/bot/extensions/adventofcode/views.py @@ -75,7 +75,7 @@ async def global_leaderboard(self, interaction: core.InteractionType, button: ui embed = discord.Embed( title="Advent of Code Global Leaderboard", colour=0x68C290, - url="https://adventofcode.com", + url=f"https://adventofcode.com/{YEAR}/leaderboard", ) if soup.find("p").text == "Nothing to show on the leaderboard... yet.": diff --git a/example.env b/example.env index fb01ca14..5c9adeb0 100644 --- a/example.env +++ b/example.env @@ -2,7 +2,6 @@ # --- AoC AOC__CHANNEL_ID=0 AOC__ROLE_ID=0 -AOC__LEADERBOARD_ID = 975452 AOC__LEADERBOARD_CODE = 975452-d90a48b0 AOC__SESSION_COOKIE=... From b9eccbafbdc1674e979d82a289e6221ad06d2131 Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Tue, 21 Nov 2023 20:18:15 +0530 Subject: [PATCH 12/18] updating role log embed --- bot/extensions/custom_roles/events.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bot/extensions/custom_roles/events.py b/bot/extensions/custom_roles/events.py index aaad6302..273c3fcc 100644 --- a/bot/extensions/custom_roles/events.py +++ b/bot/extensions/custom_roles/events.py @@ -69,12 +69,17 @@ async def on_custom_role_update(self, before: CustomRole, after: CustomRole): timestamp=datetime.datetime.utcnow(), ) - embed.add_field(name="Old Name", value=before.name) - embed.add_field(name="New Name", value=after.name) + if before.name != after.name: + embed.add_field(name="Name (Updated)", value=f"{before.name} -> {after.name}") + else: + embed.add_field(name="Name", value=before.name) embed.add_field(name="\u200B", value="\u200B") - embed.add_field(name="Old Color", value="#" + hex(before.color)[2:]) - embed.add_field(name="New Color", value="#" + hex(after.color)[2:]) + if before.color != after.color: + embed.add_field(name="Color (Updated)", value=f"#{hex(before.color)[2:]} -> #{hex(after.color)[2:]}") + else: + embed.add_field(name="Color", value="#" + hex(after.color)[2:]) embed.set_thumbnail(url=user.avatar) + embed.set_author(name=user.name, icon_url=user.avatar) embed.set_footer(text=f"user_id: {after.user_id}") return await self.custom_roles_logs_channel.send(embed=embed) From 05fb8e67b712dd7e2a2b34a9bb4fadc2562fe29f Mon Sep 17 00:00:00 2001 From: Sarthak singh Date: Tue, 21 Nov 2023 20:40:59 +0530 Subject: [PATCH 13/18] updating role log embed as per sylte's preference --- bot/extensions/custom_roles/events.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/extensions/custom_roles/events.py b/bot/extensions/custom_roles/events.py index 273c3fcc..91b3a8a4 100644 --- a/bot/extensions/custom_roles/events.py +++ b/bot/extensions/custom_roles/events.py @@ -69,15 +69,18 @@ async def on_custom_role_update(self, before: CustomRole, after: CustomRole): timestamp=datetime.datetime.utcnow(), ) - if before.name != after.name: - embed.add_field(name="Name (Updated)", value=f"{before.name} -> {after.name}") - else: + if before.name == after.name: embed.add_field(name="Name", value=before.name) - embed.add_field(name="\u200B", value="\u200B") - if before.color != after.color: - embed.add_field(name="Color (Updated)", value=f"#{hex(before.color)[2:]} -> #{hex(after.color)[2:]}") else: + embed.add_field(name="Name (Updated)", value=f"{before.name} -> {after.name}") + + embed.add_field(name="\u200B", value="\u200B") + + if before.color == after.color: embed.add_field(name="Color", value="#" + hex(after.color)[2:]) + else: + embed.add_field(name="Color (Updated)", value=f"#{hex(before.color)[2:]} -> #{hex(after.color)[2:]}") + embed.set_thumbnail(url=user.avatar) embed.set_author(name=user.name, icon_url=user.avatar) embed.set_footer(text=f"user_id: {after.user_id}") From fc519a13cc7554b1ea53495ab8592f7933893da0 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Sat, 25 Nov 2023 14:13:33 +0200 Subject: [PATCH 14/18] Fixed bot erroring if avatar doesn't exist --- bot/extensions/levelling/commands.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bot/extensions/levelling/commands.py b/bot/extensions/levelling/commands.py index 1e42b82e..3caefa29 100644 --- a/bot/extensions/levelling/commands.py +++ b/bot/extensions/levelling/commands.py @@ -115,6 +115,8 @@ async def on_message(self, message): def generate_rank_image(self, username: str, avatar_bytes: bytes, rank: int, level: int, xp: int, required_xp: int): img = Image.new("RGBA", (1000, 240)) logo = Image.open(BytesIO(avatar_bytes)).resize((200, 200)) + if logo.mode != "RGBA": + logo = logo.convert("RGBA") # Paste the default background image onto the new image img.paste(self.background, (0, 0)) @@ -282,7 +284,10 @@ async def rank(self, interaction: core.InteractionType, member: discord.Member = return await interaction.response.send_message("That user is not ranked yet...", ephemeral=True) # Fetch the user's avatar as bytes - avatar_bytes = await member.display_avatar.with_format("png").read() + try: + avatar_bytes = await member.display_avatar.with_format("png").read() + except discord.errors.NotFound: + avatar_bytes = await member.default_avatar.with_format("png").read() level = utils.get_level_for_xp(record.total_xp) prev_xp = utils.get_xp_for_level(level) From 772bce2c5703b8523ff803641bc1bc5879354e77 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Sun, 26 Nov 2023 19:55:09 +0200 Subject: [PATCH 15/18] caching leaderboard results for 1 minute --- bot/extensions/adventofcode/tasks.py | 5 ++- bot/extensions/adventofcode/utils.py | 51 ++++++++++++++++++++++------ bot/extensions/adventofcode/views.py | 27 +++------------ 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/bot/extensions/adventofcode/tasks.py b/bot/extensions/adventofcode/tasks.py index d2bbef30..57788bc7 100644 --- a/bot/extensions/adventofcode/tasks.py +++ b/bot/extensions/adventofcode/tasks.py @@ -1,6 +1,5 @@ import asyncio -import datetime as dt -from datetime import datetime +from datetime import datetime, time from zoneinfo import ZoneInfo import discord @@ -12,7 +11,7 @@ from bot.extensions.adventofcode.utils import YEAR from bot.services import http -aoc_time = dt.time(hour=0, minute=0, second=1, tzinfo=ZoneInfo("EST")) +aoc_time = time(hour=0, minute=0, second=1, tzinfo=ZoneInfo("EST")) class AdventOfCodeTasks(commands.Cog): diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py index 554d7725..131bc0a6 100644 --- a/bot/extensions/adventofcode/utils.py +++ b/bot/extensions/adventofcode/utils.py @@ -81,16 +81,45 @@ def set_name_to_anonymous(cls, val: Optional[str]) -> str: return val -async def fetch_leaderboard(local: bool = False) -> Union[str, dict]: - url = f"https://adventofcode.com/{YEAR}/leaderboard" - if local: - url += f"/private/view/{LEADERBOARD_ID}.json" - - async with http.session.get(url, headers=AOC_REQUEST_HEADERS, raise_for_status=True) as resp: - if resp.status == 200: - if local: - response = await resp.json() +class Cache: + def __init__(self): + self.global_cache = {"data": "", "expiration": datetime(1972, 1, 1)} + self.local_cache = {"data": {}, "expiration": datetime(1972, 1, 1)} + + async def fetch_leaderboard(self, local: bool = False) -> Union[str, dict]: + now = datetime.now() + + if local: + if now > self.local_cache["expiration"]: + response = await self._fetch_from_api(local=True) + self.local_cache["data"] = response + self.local_cache["expiration"] = now + timedelta(minutes=1) else: - response = await resp.text() + response = self.local_cache["data"] + else: + if now > self.global_cache["expiration"]: + response = await self._fetch_from_api(local=False) + self.global_cache["data"] = response + self.global_cache["expiration"] = now + timedelta(minutes=1) + else: + response = self.global_cache["data"] + + return response + + @staticmethod + async def _fetch_from_api(local: bool = False) -> Union[str, dict]: + url = f"https://adventofcode.com/{YEAR}/leaderboard" + if local: + url += f"/private/view/{LEADERBOARD_ID}.json" + + async with http.session.get(url, headers=AOC_REQUEST_HEADERS, raise_for_status=True) as resp: + if resp.status == 200: + if local: + response = await resp.json() + else: + response = await resp.text() + + return response + - return response +cache = Cache() diff --git a/bot/extensions/adventofcode/views.py b/bot/extensions/adventofcode/views.py index 6312a414..39de63d3 100644 --- a/bot/extensions/adventofcode/views.py +++ b/bot/extensions/adventofcode/views.py @@ -7,7 +7,7 @@ from discord import ui from bot import core -from bot.extensions.adventofcode.utils import LEADERBOARD_ID, YEAR, Member, fetch_leaderboard, home_embed, ordinal +from bot.extensions.adventofcode.utils import LEADERBOARD_ID, YEAR, Member, cache, home_embed, ordinal class CreateAdventOfCodeView(ui.View): @@ -15,21 +15,15 @@ class CreateAdventOfCodeView(ui.View): LOCAL_LEADERBOARD_CUSTOM_ID = "extensions:adventofcode:local" GLOBAL_LEADERBOARD_CUSTOM_ID = "extensions:adventofcode:global" - def reset_buttons(self): - self.children[0].disabled = True - self.children[1].disabled = False - self.children[2].disabled = False - - @discord.ui.button(label="Home", style=discord.ButtonStyle.gray, emoji="🏠", custom_id=HOME_CUSTOM_ID, disabled=True) + @discord.ui.button(label="Home", style=discord.ButtonStyle.gray, emoji="🏠", custom_id=HOME_CUSTOM_ID) async def home(self, interaction: core.InteractionType, _button: ui.Button): - self.reset_buttons() await interaction.response.edit_message(embed=home_embed(), view=self) @discord.ui.button( label="Local Leaderboard", style=discord.ButtonStyle.gray, emoji="👥", custom_id=LOCAL_LEADERBOARD_CUSTOM_ID ) async def local_leaderboard(self, interaction: core.InteractionType, button: ui.Button): - leaderboard = await fetch_leaderboard(local=True) + leaderboard = await cache.fetch_leaderboard(local=True) members = [Member(**member_data) for member_data in leaderboard["members"].values()] @@ -54,24 +48,15 @@ async def local_leaderboard(self, interaction: core.InteractionType, button: ui. ) embed.set_footer(text=f"Current Day: {datetime.now(tz=ZoneInfo('EST')).day}/25") - button.disabled = True - self.children[0].disabled = False - self.children[2].disabled = False - await interaction.response.edit_message(embed=embed, view=self) - self.reset_buttons() @discord.ui.button( label="Global Leaderboard", style=discord.ButtonStyle.gray, emoji="🌎", custom_id=GLOBAL_LEADERBOARD_CUSTOM_ID ) async def global_leaderboard(self, interaction: core.InteractionType, button: ui.Button): - raw_html = await fetch_leaderboard() + raw_html = await cache.fetch_leaderboard(local=False) soup = BeautifulSoup(raw_html, "html.parser") - button.disabled = True - self.children[0].disabled = False - self.children[1].disabled = False - embed = discord.Embed( title="Advent of Code Global Leaderboard", colour=0x68C290, @@ -80,8 +65,7 @@ async def global_leaderboard(self, interaction: core.InteractionType, button: ui if soup.find("p").text == "Nothing to show on the leaderboard... yet.": embed.description = "The global leaderboard is not available yet. Please try again later." - await interaction.response.edit_message(embed=embed, view=self) - return self.reset_buttons() + return await interaction.response.edit_message(embed=embed, view=self) ele = soup.find_all("div", class_="leaderboard-entry") exp = r"(?:[ ]{,2}(\d+)\))?[ ]+(\d+)\s+([\w\(\)\#\@\-\d ]+)" @@ -114,4 +98,3 @@ async def global_leaderboard(self, interaction: core.InteractionType, button: ui embed.description = s_desc await interaction.response.edit_message(embed=embed, view=self) - self.reset_buttons() From 74973334632249cf4713be2452ee90cd4e6c3d2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 01:01:32 +0000 Subject: [PATCH 16/18] Bump aiohttp from 3.8.6 to 3.9.0 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.6 to 3.9.0. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.6...v3.9.0) --- updated-dependencies: - dependency-name: aiohttp dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 256 ++++++++++++++++------------------------------------ 1 file changed, 80 insertions(+), 176 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8095bf38..829689e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,111 +2,99 @@ [[package]] name = "aiohttp" -version = "3.8.6" +version = "3.9.0" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1"}, - {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566"}, - {file = "aiohttp-3.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed"}, - {file = "aiohttp-3.8.6-cp310-cp310-win32.whl", hash = "sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2"}, - {file = "aiohttp-3.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f"}, - {file = "aiohttp-3.8.6-cp311-cp311-win32.whl", hash = "sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb"}, - {file = "aiohttp-3.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0"}, - {file = "aiohttp-3.8.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6"}, - {file = "aiohttp-3.8.6-cp36-cp36m-win32.whl", hash = "sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c"}, - {file = "aiohttp-3.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53"}, - {file = "aiohttp-3.8.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df"}, - {file = "aiohttp-3.8.6-cp37-cp37m-win32.whl", hash = "sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2"}, - {file = "aiohttp-3.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771"}, - {file = "aiohttp-3.8.6-cp38-cp38-win32.whl", hash = "sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f"}, - {file = "aiohttp-3.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17"}, - {file = "aiohttp-3.8.6-cp39-cp39-win32.whl", hash = "sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4"}, - {file = "aiohttp-3.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132"}, - {file = "aiohttp-3.8.6.tar.gz", hash = "sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6896b8416be9ada4d22cd359d7cb98955576ce863eadad5596b7cdfbf3e17c6c"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1736d87dad8ef46a8ec9cddd349fa9f7bd3a064c47dd6469c0d6763d3d49a4fc"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c9e5f4d7208cda1a2bb600e29069eecf857e6980d0ccc922ccf9d1372c16f4b"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8488519aa05e636c5997719fe543c8daf19f538f4fa044f3ce94bee608817cff"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ab16c254e2312efeb799bc3c06897f65a133b38b69682bf75d1f1ee1a9c43a9"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a94bde005a8f926d0fa38b88092a03dea4b4875a61fbcd9ac6f4351df1b57cd"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b777c9286b6c6a94f50ddb3a6e730deec327e9e2256cb08b5530db0f7d40fd8"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:571760ad7736b34d05597a1fd38cbc7d47f7b65deb722cb8e86fd827404d1f6b"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:deac0a32aec29608eb25d730f4bc5a261a65b6c48ded1ed861d2a1852577c932"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4ee1b4152bc3190cc40ddd6a14715e3004944263ea208229ab4c297712aa3075"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:3607375053df58ed6f23903aa10cf3112b1240e8c799d243bbad0f7be0666986"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:65b0a70a25456d329a5e1426702dde67be0fb7a4ead718005ba2ca582d023a94"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a2eb5311a37fe105aa35f62f75a078537e1a9e4e1d78c86ec9893a3c97d7a30"}, + {file = "aiohttp-3.9.0-cp310-cp310-win32.whl", hash = "sha256:2cbc14a13fb6b42d344e4f27746a4b03a2cb0c1c3c5b932b0d6ad8881aa390e3"}, + {file = "aiohttp-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac9669990e2016d644ba8ae4758688534aabde8dbbc81f9af129c3f5f01ca9cd"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8e05f5163528962ce1d1806fce763ab893b1c5b7ace0a3538cd81a90622f844"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4afa8f71dba3a5a2e1e1282a51cba7341ae76585345c43d8f0e624882b622218"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f929f4c9b9a00f3e6cc0587abb95ab9c05681f8b14e0fe1daecfa83ea90f8318"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28185e36a78d247c55e9fbea2332d16aefa14c5276a582ce7a896231c6b1c208"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a486ddf57ab98b6d19ad36458b9f09e6022de0381674fe00228ca7b741aacb2f"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70e851f596c00f40a2f00a46126c95c2e04e146015af05a9da3e4867cfc55911"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5b7bf8fe4d39886adc34311a233a2e01bc10eb4e842220235ed1de57541a896"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67a51ea415192c2e53e4e048c78bab82d21955b4281d297f517707dc836bf3d"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:694df243f394629bcae2d8ed94c589a181e8ba8604159e6e45e7b22e58291113"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3dd8119752dd30dd7bca7d4bc2a92a59be6a003e4e5c2cf7e248b89751b8f4b7"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:eb6dfd52063186ac97b4caa25764cdbcdb4b10d97f5c5f66b0fa95052e744eb7"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d97c3e286d0ac9af6223bc132dc4bad6540b37c8d6c0a15fe1e70fb34f9ec411"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:816f4db40555026e4cdda604a1088577c1fb957d02f3f1292e0221353403f192"}, + {file = "aiohttp-3.9.0-cp311-cp311-win32.whl", hash = "sha256:3abf0551874fecf95f93b58f25ef4fc9a250669a2257753f38f8f592db85ddea"}, + {file = "aiohttp-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:e18d92c3e9e22553a73e33784fcb0ed484c9874e9a3e96c16a8d6a1e74a0217b"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:99ae01fb13a618b9942376df77a1f50c20a281390dad3c56a6ec2942e266220d"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317719d7f824eba55857fe0729363af58e27c066c731bc62cd97bc9c3d9c7ea4"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e3b3c107ccb0e537f309f719994a55621acd2c8fdf6d5ce5152aed788fb940"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45820ddbb276113ead8d4907a7802adb77548087ff5465d5c554f9aa3928ae7d"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a183f1978802588711aed0dea31e697d760ce9055292db9dc1604daa9a8ded"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a4cd44788ea0b5e6bb8fa704597af3a30be75503a7ed1098bc5b8ffdf6c982"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673343fbc0c1ac44d0d2640addc56e97a052504beacd7ade0dc5e76d3a4c16e8"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e8a3b79b6d186a9c99761fd4a5e8dd575a48d96021f220ac5b5fa856e5dd029"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6777a390e41e78e7c45dab43a4a0196c55c3b8c30eebe017b152939372a83253"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7ae5f99a32c53731c93ac3075abd3e1e5cfbe72fc3eaac4c27c9dd64ba3b19fe"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f1e4f254e9c35d8965d377e065c4a8a55d396fe87c8e7e8429bcfdeeb229bfb3"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11ca808f9a6b63485059f5f6e164ef7ec826483c1212a44f268b3653c91237d8"}, + {file = "aiohttp-3.9.0-cp312-cp312-win32.whl", hash = "sha256:de3cc86f4ea8b4c34a6e43a7306c40c1275e52bfa9748d869c6b7d54aa6dad80"}, + {file = "aiohttp-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca4fddf84ac7d8a7d0866664936f93318ff01ee33e32381a115b19fb5a4d1202"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f09960b5bb1017d16c0f9e9f7fc42160a5a49fa1e87a175fd4a2b1a1833ea0af"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8303531e2c17b1a494ffaeba48f2da655fe932c4e9a2626c8718403c83e5dd2b"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4790e44f46a4aa07b64504089def5744d3b6780468c4ec3a1a36eb7f2cae9814"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1d7edf74a36de0e5ca50787e83a77cf352f5504eb0ffa3f07000a911ba353fb"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94697c7293199c2a2551e3e3e18438b4cba293e79c6bc2319f5fd652fccb7456"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1b66dbb8a7d5f50e9e2ea3804b01e766308331d0cac76eb30c563ac89c95985"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9623cfd9e85b76b83ef88519d98326d4731f8d71869867e47a0b979ffec61c73"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f32c86dc967ab8c719fd229ce71917caad13cc1e8356ee997bf02c5b368799bf"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f50b4663c3e0262c3a361faf440761fbef60ccdde5fe8545689a4b3a3c149fb4"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dcf71c55ec853826cd70eadb2b6ac62ec577416442ca1e0a97ad875a1b3a0305"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:42fe4fd9f0dfcc7be4248c162d8056f1d51a04c60e53366b0098d1267c4c9da8"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76a86a9989ebf82ee61e06e2bab408aec4ea367dc6da35145c3352b60a112d11"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887"}, + {file = "aiohttp-3.9.0-cp38-cp38-win32.whl", hash = "sha256:a00ce44c21612d185c5275c5cba4bab8d7c1590f248638b667ed8a782fa8cd6f"}, + {file = "aiohttp-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b9345ab92ebe6003ae11d8092ce822a0242146e6fa270889b9ba965457ca40"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98d21092bf2637c5fa724a428a69e8f5955f2182bff61f8036827cf6ce1157bf"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35a68cd63ca6aaef5707888f17a70c36efe62b099a4e853d33dc2e9872125be8"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7f6235c7475658acfc1769d968e07ab585c79f6ca438ddfecaa9a08006aee2"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db04d1de548f7a62d1dd7e7cdf7c22893ee168e22701895067a28a8ed51b3735"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:536b01513d67d10baf6f71c72decdf492fb7433c5f2f133e9a9087379d4b6f31"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c8b0a6487e8109427ccf638580865b54e2e3db4a6e0e11c02639231b41fc0f"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7276fe0017664414fdc3618fca411630405f1aaf0cc3be69def650eb50441787"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23170247ef89ffa842a02bbfdc425028574d9e010611659abeb24d890bc53bb8"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1a2ea8252cacc7fd51df5a56d7a2bb1986ed39be9397b51a08015727dfb69bd"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d71abc15ff7047412ef26bf812dfc8d0d1020d664617f4913df2df469f26b76"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2d820162c8c2bdbe97d328cd4f417c955ca370027dce593345e437b2e9ffdc4d"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:2779f5e7c70f7b421915fd47db332c81de365678180a9f3ab404088f87ba5ff9"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:366bc870d7ac61726f32a489fbe3d1d8876e87506870be66b01aeb84389e967e"}, + {file = "aiohttp-3.9.0-cp39-cp39-win32.whl", hash = "sha256:1df43596b826022b14998f0460926ce261544fedefe0d2f653e1b20f49e96454"}, + {file = "aiohttp-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c196b30f1b1aa3363a69dd69079ae9bec96c2965c4707eaa6914ba099fb7d4f"}, + {file = "aiohttp-3.9.0.tar.gz", hash = "sha256:09f23292d29135025e19e8ff4f0a68df078fe4ee013bca0105b2e803989de92d"}, ] [package.dependencies] aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] +speedups = ["Brotli", "aiodns", "brotlicffi"] [[package]] name = "aiosignal" @@ -302,90 +290,6 @@ files = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -[[package]] -name = "charset-normalizer" -version = "3.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, -] - [[package]] name = "click" version = "8.1.3" From 5a492e408c6e3b3b37246c711592b61db6afd2df Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:00:15 +0200 Subject: [PATCH 17/18] switched to using async-lru --- bot/extensions/adventofcode/utils.py | 51 +++++++--------------------- bot/extensions/adventofcode/views.py | 6 ++-- poetry.lock | 16 ++++++++- pyproject.toml | 1 + 4 files changed, 31 insertions(+), 43 deletions(-) diff --git a/bot/extensions/adventofcode/utils.py b/bot/extensions/adventofcode/utils.py index 131bc0a6..ef3921ea 100644 --- a/bot/extensions/adventofcode/utils.py +++ b/bot/extensions/adventofcode/utils.py @@ -3,6 +3,7 @@ from zoneinfo import ZoneInfo import discord +from async_lru import alru_cache from pydantic import BaseModel, validator from bot.config import settings @@ -81,45 +82,17 @@ def set_name_to_anonymous(cls, val: Optional[str]) -> str: return val -class Cache: - def __init__(self): - self.global_cache = {"data": "", "expiration": datetime(1972, 1, 1)} - self.local_cache = {"data": {}, "expiration": datetime(1972, 1, 1)} +@alru_cache(ttl=60) +async def fetch_leaderboard(local: bool = False) -> Union[str, dict]: + url = f"https://adventofcode.com/{YEAR}/leaderboard" + if local: + url += f"/private/view/{LEADERBOARD_ID}.json" - async def fetch_leaderboard(self, local: bool = False) -> Union[str, dict]: - now = datetime.now() - - if local: - if now > self.local_cache["expiration"]: - response = await self._fetch_from_api(local=True) - self.local_cache["data"] = response - self.local_cache["expiration"] = now + timedelta(minutes=1) - else: - response = self.local_cache["data"] - else: - if now > self.global_cache["expiration"]: - response = await self._fetch_from_api(local=False) - self.global_cache["data"] = response - self.global_cache["expiration"] = now + timedelta(minutes=1) + async with http.session.get(url, headers=AOC_REQUEST_HEADERS, raise_for_status=True) as resp: + if resp.status == 200: + if local: + response = await resp.json() else: - response = self.global_cache["data"] - - return response - - @staticmethod - async def _fetch_from_api(local: bool = False) -> Union[str, dict]: - url = f"https://adventofcode.com/{YEAR}/leaderboard" - if local: - url += f"/private/view/{LEADERBOARD_ID}.json" - - async with http.session.get(url, headers=AOC_REQUEST_HEADERS, raise_for_status=True) as resp: - if resp.status == 200: - if local: - response = await resp.json() - else: - response = await resp.text() - - return response - + response = await resp.text() -cache = Cache() + return response diff --git a/bot/extensions/adventofcode/views.py b/bot/extensions/adventofcode/views.py index 39de63d3..13e7da4d 100644 --- a/bot/extensions/adventofcode/views.py +++ b/bot/extensions/adventofcode/views.py @@ -7,7 +7,7 @@ from discord import ui from bot import core -from bot.extensions.adventofcode.utils import LEADERBOARD_ID, YEAR, Member, cache, home_embed, ordinal +from bot.extensions.adventofcode.utils import LEADERBOARD_ID, YEAR, Member, fetch_leaderboard, home_embed, ordinal class CreateAdventOfCodeView(ui.View): @@ -23,7 +23,7 @@ async def home(self, interaction: core.InteractionType, _button: ui.Button): label="Local Leaderboard", style=discord.ButtonStyle.gray, emoji="👥", custom_id=LOCAL_LEADERBOARD_CUSTOM_ID ) async def local_leaderboard(self, interaction: core.InteractionType, button: ui.Button): - leaderboard = await cache.fetch_leaderboard(local=True) + leaderboard = await fetch_leaderboard(local=True) members = [Member(**member_data) for member_data in leaderboard["members"].values()] @@ -54,7 +54,7 @@ async def local_leaderboard(self, interaction: core.InteractionType, button: ui. label="Global Leaderboard", style=discord.ButtonStyle.gray, emoji="🌎", custom_id=GLOBAL_LEADERBOARD_CUSTOM_ID ) async def global_leaderboard(self, interaction: core.InteractionType, button: ui.Button): - raw_html = await cache.fetch_leaderboard(local=False) + raw_html = await fetch_leaderboard(local=False) soup = BeautifulSoup(raw_html, "html.parser") embed = discord.Embed( diff --git a/poetry.lock b/poetry.lock index a7b6ac39..37aede9f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -137,6 +137,20 @@ files = [ six = ">=1.6.1,<2.0" wheel = ">=0.23.0,<1.0" +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + [[package]] name = "async-timeout" version = "4.0.2" @@ -1331,4 +1345,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "0faf30752db0cb31ba0d77ff3b1ede7423c660351dc1f9d036870cd7ec86ef5e" +content-hash = "3f63299e4f321c26e411a6af6ec89184569f08009d6b6455f218c04917a3247f" diff --git a/pyproject.toml b/pyproject.toml index c7986713..f88bf540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ beautifulsoup4 = "^4.12.2" tabulate = "^0.9.0" pillow = "^10.1.0" tzdata = "^2023.3" +async-lru = "^2.0.4" [tool.poetry.group.dev.dependencies] isort = "^5.12.0" From c3fc873339dcedb9ba6ef3a7318bfd0dbd25c5b9 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:57:06 +0200 Subject: [PATCH 18/18] update aiohttp --- poetry.lock | 258 +++++++++++++++++----------------------------------- 1 file changed, 81 insertions(+), 177 deletions(-) diff --git a/poetry.lock b/poetry.lock index 37aede9f..6777ff9c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,111 +2,99 @@ [[package]] name = "aiohttp" -version = "3.8.6" +version = "3.9.0" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1"}, - {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566"}, - {file = "aiohttp-3.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47"}, - {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2"}, - {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed"}, - {file = "aiohttp-3.8.6-cp310-cp310-win32.whl", hash = "sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2"}, - {file = "aiohttp-3.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca"}, - {file = "aiohttp-3.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d"}, - {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887"}, - {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f"}, - {file = "aiohttp-3.8.6-cp311-cp311-win32.whl", hash = "sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb"}, - {file = "aiohttp-3.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0"}, - {file = "aiohttp-3.8.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07"}, - {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071"}, - {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6"}, - {file = "aiohttp-3.8.6-cp36-cp36m-win32.whl", hash = "sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c"}, - {file = "aiohttp-3.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53"}, - {file = "aiohttp-3.8.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460"}, - {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8"}, - {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df"}, - {file = "aiohttp-3.8.6-cp37-cp37m-win32.whl", hash = "sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2"}, - {file = "aiohttp-3.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62"}, - {file = "aiohttp-3.8.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae"}, - {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921"}, - {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771"}, - {file = "aiohttp-3.8.6-cp38-cp38-win32.whl", hash = "sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f"}, - {file = "aiohttp-3.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5"}, - {file = "aiohttp-3.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1"}, - {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e"}, - {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17"}, - {file = "aiohttp-3.8.6-cp39-cp39-win32.whl", hash = "sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4"}, - {file = "aiohttp-3.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132"}, - {file = "aiohttp-3.8.6.tar.gz", hash = "sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6896b8416be9ada4d22cd359d7cb98955576ce863eadad5596b7cdfbf3e17c6c"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1736d87dad8ef46a8ec9cddd349fa9f7bd3a064c47dd6469c0d6763d3d49a4fc"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c9e5f4d7208cda1a2bb600e29069eecf857e6980d0ccc922ccf9d1372c16f4b"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8488519aa05e636c5997719fe543c8daf19f538f4fa044f3ce94bee608817cff"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ab16c254e2312efeb799bc3c06897f65a133b38b69682bf75d1f1ee1a9c43a9"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a94bde005a8f926d0fa38b88092a03dea4b4875a61fbcd9ac6f4351df1b57cd"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b777c9286b6c6a94f50ddb3a6e730deec327e9e2256cb08b5530db0f7d40fd8"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:571760ad7736b34d05597a1fd38cbc7d47f7b65deb722cb8e86fd827404d1f6b"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:deac0a32aec29608eb25d730f4bc5a261a65b6c48ded1ed861d2a1852577c932"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4ee1b4152bc3190cc40ddd6a14715e3004944263ea208229ab4c297712aa3075"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:3607375053df58ed6f23903aa10cf3112b1240e8c799d243bbad0f7be0666986"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:65b0a70a25456d329a5e1426702dde67be0fb7a4ead718005ba2ca582d023a94"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a2eb5311a37fe105aa35f62f75a078537e1a9e4e1d78c86ec9893a3c97d7a30"}, + {file = "aiohttp-3.9.0-cp310-cp310-win32.whl", hash = "sha256:2cbc14a13fb6b42d344e4f27746a4b03a2cb0c1c3c5b932b0d6ad8881aa390e3"}, + {file = "aiohttp-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac9669990e2016d644ba8ae4758688534aabde8dbbc81f9af129c3f5f01ca9cd"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8e05f5163528962ce1d1806fce763ab893b1c5b7ace0a3538cd81a90622f844"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4afa8f71dba3a5a2e1e1282a51cba7341ae76585345c43d8f0e624882b622218"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f929f4c9b9a00f3e6cc0587abb95ab9c05681f8b14e0fe1daecfa83ea90f8318"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28185e36a78d247c55e9fbea2332d16aefa14c5276a582ce7a896231c6b1c208"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a486ddf57ab98b6d19ad36458b9f09e6022de0381674fe00228ca7b741aacb2f"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70e851f596c00f40a2f00a46126c95c2e04e146015af05a9da3e4867cfc55911"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5b7bf8fe4d39886adc34311a233a2e01bc10eb4e842220235ed1de57541a896"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67a51ea415192c2e53e4e048c78bab82d21955b4281d297f517707dc836bf3d"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:694df243f394629bcae2d8ed94c589a181e8ba8604159e6e45e7b22e58291113"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3dd8119752dd30dd7bca7d4bc2a92a59be6a003e4e5c2cf7e248b89751b8f4b7"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:eb6dfd52063186ac97b4caa25764cdbcdb4b10d97f5c5f66b0fa95052e744eb7"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d97c3e286d0ac9af6223bc132dc4bad6540b37c8d6c0a15fe1e70fb34f9ec411"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:816f4db40555026e4cdda604a1088577c1fb957d02f3f1292e0221353403f192"}, + {file = "aiohttp-3.9.0-cp311-cp311-win32.whl", hash = "sha256:3abf0551874fecf95f93b58f25ef4fc9a250669a2257753f38f8f592db85ddea"}, + {file = "aiohttp-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:e18d92c3e9e22553a73e33784fcb0ed484c9874e9a3e96c16a8d6a1e74a0217b"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:99ae01fb13a618b9942376df77a1f50c20a281390dad3c56a6ec2942e266220d"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317719d7f824eba55857fe0729363af58e27c066c731bc62cd97bc9c3d9c7ea4"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e3b3c107ccb0e537f309f719994a55621acd2c8fdf6d5ce5152aed788fb940"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45820ddbb276113ead8d4907a7802adb77548087ff5465d5c554f9aa3928ae7d"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a183f1978802588711aed0dea31e697d760ce9055292db9dc1604daa9a8ded"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a4cd44788ea0b5e6bb8fa704597af3a30be75503a7ed1098bc5b8ffdf6c982"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673343fbc0c1ac44d0d2640addc56e97a052504beacd7ade0dc5e76d3a4c16e8"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e8a3b79b6d186a9c99761fd4a5e8dd575a48d96021f220ac5b5fa856e5dd029"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6777a390e41e78e7c45dab43a4a0196c55c3b8c30eebe017b152939372a83253"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7ae5f99a32c53731c93ac3075abd3e1e5cfbe72fc3eaac4c27c9dd64ba3b19fe"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f1e4f254e9c35d8965d377e065c4a8a55d396fe87c8e7e8429bcfdeeb229bfb3"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11ca808f9a6b63485059f5f6e164ef7ec826483c1212a44f268b3653c91237d8"}, + {file = "aiohttp-3.9.0-cp312-cp312-win32.whl", hash = "sha256:de3cc86f4ea8b4c34a6e43a7306c40c1275e52bfa9748d869c6b7d54aa6dad80"}, + {file = "aiohttp-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca4fddf84ac7d8a7d0866664936f93318ff01ee33e32381a115b19fb5a4d1202"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f09960b5bb1017d16c0f9e9f7fc42160a5a49fa1e87a175fd4a2b1a1833ea0af"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8303531e2c17b1a494ffaeba48f2da655fe932c4e9a2626c8718403c83e5dd2b"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4790e44f46a4aa07b64504089def5744d3b6780468c4ec3a1a36eb7f2cae9814"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1d7edf74a36de0e5ca50787e83a77cf352f5504eb0ffa3f07000a911ba353fb"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94697c7293199c2a2551e3e3e18438b4cba293e79c6bc2319f5fd652fccb7456"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1b66dbb8a7d5f50e9e2ea3804b01e766308331d0cac76eb30c563ac89c95985"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9623cfd9e85b76b83ef88519d98326d4731f8d71869867e47a0b979ffec61c73"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f32c86dc967ab8c719fd229ce71917caad13cc1e8356ee997bf02c5b368799bf"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f50b4663c3e0262c3a361faf440761fbef60ccdde5fe8545689a4b3a3c149fb4"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dcf71c55ec853826cd70eadb2b6ac62ec577416442ca1e0a97ad875a1b3a0305"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:42fe4fd9f0dfcc7be4248c162d8056f1d51a04c60e53366b0098d1267c4c9da8"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76a86a9989ebf82ee61e06e2bab408aec4ea367dc6da35145c3352b60a112d11"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887"}, + {file = "aiohttp-3.9.0-cp38-cp38-win32.whl", hash = "sha256:a00ce44c21612d185c5275c5cba4bab8d7c1590f248638b667ed8a782fa8cd6f"}, + {file = "aiohttp-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b9345ab92ebe6003ae11d8092ce822a0242146e6fa270889b9ba965457ca40"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98d21092bf2637c5fa724a428a69e8f5955f2182bff61f8036827cf6ce1157bf"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35a68cd63ca6aaef5707888f17a70c36efe62b099a4e853d33dc2e9872125be8"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7f6235c7475658acfc1769d968e07ab585c79f6ca438ddfecaa9a08006aee2"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db04d1de548f7a62d1dd7e7cdf7c22893ee168e22701895067a28a8ed51b3735"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:536b01513d67d10baf6f71c72decdf492fb7433c5f2f133e9a9087379d4b6f31"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c8b0a6487e8109427ccf638580865b54e2e3db4a6e0e11c02639231b41fc0f"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7276fe0017664414fdc3618fca411630405f1aaf0cc3be69def650eb50441787"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23170247ef89ffa842a02bbfdc425028574d9e010611659abeb24d890bc53bb8"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1a2ea8252cacc7fd51df5a56d7a2bb1986ed39be9397b51a08015727dfb69bd"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d71abc15ff7047412ef26bf812dfc8d0d1020d664617f4913df2df469f26b76"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2d820162c8c2bdbe97d328cd4f417c955ca370027dce593345e437b2e9ffdc4d"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:2779f5e7c70f7b421915fd47db332c81de365678180a9f3ab404088f87ba5ff9"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:366bc870d7ac61726f32a489fbe3d1d8876e87506870be66b01aeb84389e967e"}, + {file = "aiohttp-3.9.0-cp39-cp39-win32.whl", hash = "sha256:1df43596b826022b14998f0460926ce261544fedefe0d2f653e1b20f49e96454"}, + {file = "aiohttp-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c196b30f1b1aa3363a69dd69079ae9bec96c2965c4707eaa6914ba099fb7d4f"}, + {file = "aiohttp-3.9.0.tar.gz", hash = "sha256:09f23292d29135025e19e8ff4f0a68df078fe4ee013bca0105b2e803989de92d"}, ] [package.dependencies] aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] +speedups = ["Brotli", "aiodns", "brotlicffi"] [[package]] name = "aiosignal" @@ -316,90 +304,6 @@ files = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -[[package]] -name = "charset-normalizer" -version = "3.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, -] - [[package]] name = "click" version = "8.1.3" @@ -1345,4 +1249,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3f63299e4f321c26e411a6af6ec89184569f08009d6b6455f218c04917a3247f" +content-hash = "a372526a7151afd23a0758b9fa3cd34e9a82e5f5d810c9d8ed9e783b57897e61"