From 5eaef4b731f13b93bb848cf13c5d718d048e8697 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Fri, 27 Sep 2024 15:32:57 +0200 Subject: [PATCH 01/51] :sparkles: Improve error handling --- eadk_discord/bot_setup.py | 136 ++++++++++++++++----------------- eadk_discord/date_converter.py | 12 ++- 2 files changed, 73 insertions(+), 75 deletions(-) diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index ec9ca98..6f2415d 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -12,7 +12,7 @@ from eadk_discord import fmt from eadk_discord.database import Database -from eadk_discord.date_converter import DateConverter +from eadk_discord.date_converter import DateConverter, DateParseError from eadk_discord.event import BookDesk, Event, MakeFlex, MakeOwned, SetNumDesks, UnbookDesk from eadk_discord.state import Day, HandleEventError, State @@ -27,7 +27,7 @@ def format_date(date: date) -> str: return date.isoformat() -def get_booking_date(booking_date_arg: date | str | None) -> date | str: +def get_booking_date(booking_date_arg: date | None) -> date: """ Returns a tuple of two values: - A boolean indicating whether it is currently before 17:00. @@ -41,12 +41,9 @@ def get_booking_date(booking_date_arg: date | str | None) -> date | str: async def handle_date( - database: State, interaction: Interaction, booking_date_arg: date | str | None + database: State, interaction: Interaction, booking_date_arg: date | None ) -> tuple[date, Day] | None: booking_date = get_booking_date(booking_date_arg) - if isinstance(booking_date, str): - await interaction.response.send_message(booking_date) - return None if booking_date < database.start_date: await interaction.response.send_message( f"Date {format_date(booking_date)} is not in the database. " @@ -94,7 +91,7 @@ def setup_bot(database_path: Path, guilds: list[Snowflake]) -> Bot: @app_commands.check(channel_check) async def info( interaction: Interaction, - booking_date_arg: Transform[date | str, DateConverter] | None, + booking_date_arg: Transform[date, DateConverter] | None, ) -> None: handle_date_result = await handle_date(database.state, interaction, booking_date_arg) if handle_date_result is None: @@ -125,7 +122,7 @@ async def info( @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID, EADK_DESK_REGULAR_ID) async def book( interaction: Interaction, - booking_date_arg: Transform[date | str, DateConverter] | None, + booking_date_arg: Transform[date, DateConverter] | None, user: Member | None, desk: Range[int, 1] | None, ) -> None: @@ -154,12 +151,6 @@ async def book( return desk_index = desk - 1 desk_num = desk - if booking_day.desk(desk_index).booker: - await interaction.response.send_message( - f"Desk {desk} is already booked by {booking_day.desk(desk_index).booker} on {date_str}.", - ephemeral=True, - ) - return else: desk_index_option = booking_day.get_available_desk() if desk_index_option is not None: @@ -170,19 +161,16 @@ async def book( f"No more desks are available for booking on {date_str}.", ephemeral=True ) return - try: - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=BookDesk(date=booking_date, desk_index=desk_index, user=user_id), - ) - ) - await interaction.response.send_message( - f"Desk {desk_num} has been booked for {fmt.user(interaction, user_id)} on {date_str}." + database.handle_event( + Event( + author=author_id(interaction), + time=datetime.now(), + event=BookDesk(date=booking_date, desk_index=desk_index, user=user_id), ) - except HandleEventError as e: - await interaction.response.send_message(e.message(lambda id: fmt.user(interaction, id))) + ) + await interaction.response.send_message( + f"Desk {desk_num} has been booked for {fmt.user(interaction, user_id)} on {date_str}." + ) database.save(database_path) @bot.tree.command(name="unbook", description="Unbook a desk.", guilds=guilds) @@ -193,7 +181,7 @@ async def book( @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID, EADK_DESK_REGULAR_ID) async def unbook( interaction: Interaction, - booking_date_arg: Transform[date | str, DateConverter] | None, + booking_date_arg: Transform[date, DateConverter] | None, user: Member | None, desk: Range[int, 1] | None, ) -> None: @@ -235,29 +223,24 @@ async def unbook( desk_num = desk_index + 1 else: await interaction.response.send_message( - f"{user_id} already has no desks booked for {date_str}.", ephemeral=True + f"{fmt.user(interaction, user_id)} already has no desks booked for {date_str}.", ephemeral=True ) return - try: - desk_booker = booking_day.desk(desk_index).booker - if desk_booker: - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=UnbookDesk(date=booking_date, desk_index=desk_index), - ) - ) - await interaction.response.send_message( - f"Desk {desk_num} is no longer booked for {fmt.user(interaction, desk_booker)} on {date_str}." - ) - else: - await interaction.response.send_message( - f"Desk {desk_num} is already free on {date_str}.", ephemeral=True + desk_booker = booking_day.desk(desk_index).booker + if desk_booker: + database.handle_event( + Event( + author=author_id(interaction), + time=datetime.now(), + event=UnbookDesk(date=booking_date, desk_index=desk_index), ) - except HandleEventError as e: - await interaction.response.send_message(e.message(lambda id: fmt.user(interaction, id))) + ) + await interaction.response.send_message( + f"Desk {desk_num} is no longer booked for {fmt.user(interaction, desk_booker)} on {date_str}." + ) + else: + await interaction.response.send_message(f"Desk {desk_num} is already free on {date_str}.", ephemeral=True) database.save(database_path) @bot.tree.command( @@ -269,7 +252,7 @@ async def unbook( @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID) async def makeowned( interaction: Interaction, - start_date: Transform[date | str, DateConverter], + start_date: Transform[date, DateConverter], user: Member | None, desk: Range[int, 1], ) -> None: @@ -298,19 +281,16 @@ async def makeowned( user_id = user.id else: user_id = author_id(interaction) - try: - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=MakeOwned(start_date=booking_date, desk_index=desk_index, user=user_id), - ) - ) - await interaction.response.send_message( - f"Desk {desk} is now owned by {fmt.user(interaction, user_id)} from {date_str} onwards." + database.handle_event( + Event( + author=author_id(interaction), + time=datetime.now(), + event=MakeOwned(start_date=booking_date, desk_index=desk_index, user=user_id), ) - except HandleEventError as e: - await interaction.response.send_message(e.message(lambda id: fmt.user(interaction, id))) + ) + await interaction.response.send_message( + f"Desk {desk} is now owned by {fmt.user(interaction, user_id)} from {date_str} onwards." + ) database.save(database_path) @bot.tree.command( @@ -321,7 +301,7 @@ async def makeowned( @app_commands.check(channel_check) @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID) async def makeflex( - interaction: Interaction, start_date: Transform[date | str, DateConverter], desk: Range[int, 1] + interaction: Interaction, start_date: Transform[date, DateConverter], desk: Range[int, 1] ) -> None: handle_date_result = await handle_date(database.state, interaction, start_date) if handle_date_result is None: @@ -344,17 +324,14 @@ async def makeflex( desk_index = desk - 1 - try: - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=MakeFlex(start_date=booking_date, desk_index=desk_index), - ) + database.handle_event( + Event( + author=author_id(interaction), + time=datetime.now(), + event=MakeFlex(start_date=booking_date, desk_index=desk_index), ) - await interaction.response.send_message(f"Desk {desk} is now a flex desk from {date_str} onwards.") - except HandleEventError as e: - await interaction.response.send_message(e.message(lambda id: fmt.user(interaction, id))) + ) + await interaction.response.send_message(f"Desk {desk} is now a flex desk from {date_str} onwards.") database.save(database_path) @bot.command() @@ -378,7 +355,6 @@ async def on_ready() -> None: @bot.tree.error async def on_error(interaction: Interaction, error: AppCommandError) -> None: - print(type(error)) if isinstance(error, discord.app_commands.errors.MissingAnyRole) or isinstance( error, discord.app_commands.errors.MissingRole ): @@ -389,8 +365,24 @@ async def on_error(interaction: Interaction, error: AppCommandError) -> None: "This command can only be used in the office channel.", ephemeral=True ) return + if isinstance(error, discord.app_commands.errors.TransformerError): + match error.__cause__: + case DateParseError(arg): + await interaction.response.send_message( + f"Date {arg} could not be parsed. " + "Please use the format YYYY-MM-DD, 'today', 'tomorrow', or specify a weekday.", + ephemeral=True, + ) + return + if isinstance(error, discord.app_commands.errors.CommandInvokeError): + match error.__cause__: + case HandleEventError(_event, event_error): + await interaction.response.send_message( + event_error.message(lambda id: fmt.user(interaction, id)), ephemeral=True + ) + return else: await interaction.response.send_message("INTERNAL ERROR HAS OCCURRED BEEP BOOP", ephemeral=True) - return + raise error return bot diff --git a/eadk_discord/date_converter.py b/eadk_discord/date_converter.py index 5d0fd45..19d70f3 100644 --- a/eadk_discord/date_converter.py +++ b/eadk_discord/date_converter.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from datetime import date, timedelta from discord import Interaction @@ -14,7 +15,12 @@ ] -def parse_date_arg(argument: str, today: date) -> date | str: +@dataclass +class DateParseError(Exception): + argument: str + + +def parse_date_arg(argument: str, today: date) -> date: if argument.lower() == "today": return today elif argument.lower() == "tomorrow": @@ -27,10 +33,10 @@ def parse_date_arg(argument: str, today: date) -> date | str: try: return date.fromisoformat(argument) except Exception: - return "Could not parse date argument.\nPlease format the date as YYYY-MM-DD" + raise DateParseError(argument) from Exception class DateConverter(Transformer): - async def transform(self, interaction: Interaction, argument: str) -> date | str: + async def transform(self, interaction: Interaction, argument: str) -> date: today = date.today() return parse_date_arg(argument, today) From 2b45de0f8a38d50a860925b1c541f06d52bafae2 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Fri, 27 Sep 2024 15:43:21 +0200 Subject: [PATCH 02/51] :sparkles: Improve date handling --- eadk_discord/bot_setup.py | 46 ++++++++++----------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index 6f2415d..f633b32 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -14,7 +14,7 @@ from eadk_discord.database import Database from eadk_discord.date_converter import DateConverter, DateParseError from eadk_discord.event import BookDesk, Event, MakeFlex, MakeOwned, SetNumDesks, UnbookDesk -from eadk_discord.state import Day, HandleEventError, State +from eadk_discord.state import HandleEventError TIME_ZONE = ZoneInfo("Europe/Copenhagen") @@ -40,20 +40,6 @@ def get_booking_date(booking_date_arg: date | None) -> date: return booking_date -async def handle_date( - database: State, interaction: Interaction, booking_date_arg: date | None -) -> tuple[date, Day] | None: - booking_date = get_booking_date(booking_date_arg) - if booking_date < database.start_date: - await interaction.response.send_message( - f"Date {format_date(booking_date)} is not in the database. " - f"The database starts at {format_date(database.start_date)}." - ) - return None - booking_day, _ = database.day(booking_date) - return booking_date, booking_day - - def author_id(interaction: Interaction) -> int: return interaction.user.id @@ -93,10 +79,8 @@ async def info( interaction: Interaction, booking_date_arg: Transform[date, DateConverter] | None, ) -> None: - handle_date_result = await handle_date(database.state, interaction, booking_date_arg) - if handle_date_result is None: - return - booking_date, booking_day = handle_date_result + booking_date = get_booking_date(booking_date_arg) + booking_day, _ = database.state.day(booking_date) desk_numbers_str = "\n".join(str(i + 1) for i in range(len(booking_day.desks))) desk_bookers_str = "\n".join( @@ -126,10 +110,8 @@ async def book( user: Member | None, desk: Range[int, 1] | None, ) -> None: - handle_date_result = await handle_date(database.state, interaction, booking_date_arg) - if handle_date_result is None: - return - booking_date, booking_day = handle_date_result + booking_date = get_booking_date(booking_date_arg) + booking_day, _ = database.state.day(booking_date) date_str = format_date(booking_date) if booking_date < date.today(): @@ -185,10 +167,8 @@ async def unbook( user: Member | None, desk: Range[int, 1] | None, ) -> None: - handle_date_result = await handle_date(database.state, interaction, booking_date_arg) - if handle_date_result is None: - return - booking_date, booking_day = handle_date_result + booking_date = get_booking_date(booking_date_arg) + booking_day, _ = database.state.day(booking_date) date_str = format_date(booking_date) if booking_date < date.today(): @@ -256,10 +236,8 @@ async def makeowned( user: Member | None, desk: Range[int, 1], ) -> None: - handle_date_result = await handle_date(database.state, interaction, start_date) - if handle_date_result is None: - return - booking_date, booking_day = handle_date_result + booking_date = get_booking_date(start_date) + booking_day, _ = database.state.day(booking_date) date_str = format_date(booking_date) if booking_date < date.today(): @@ -303,10 +281,8 @@ async def makeowned( async def makeflex( interaction: Interaction, start_date: Transform[date, DateConverter], desk: Range[int, 1] ) -> None: - handle_date_result = await handle_date(database.state, interaction, start_date) - if handle_date_result is None: - return - booking_date, booking_day = handle_date_result + booking_date = get_booking_date(start_date) + booking_day, _ = database.state.day(booking_date) date_str = format_date(booking_date) if booking_date < date.today(): From 680ea432a3e3f6b3803f25c2ed3b7e776ee9f4ad Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Tue, 1 Oct 2024 17:57:28 +0200 Subject: [PATCH 03/51] :see_no_evil: Expand possible database names --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3d98a85..11d110d 100644 --- a/.gitignore +++ b/.gitignore @@ -161,7 +161,7 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -db.json +db*.json .vscode/ .ruff_cache/ From 6e7a62e85e311db17abac511781bb26f28ca7144 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Tue, 1 Oct 2024 22:10:20 +0200 Subject: [PATCH 04/51] :recycle: Separate command logic from IO --- eadk_discord/bot.py | 224 +++++++++++++++++++++++++++++ eadk_discord/bot_setup.py | 287 ++++++-------------------------------- eadk_discord/dates.py | 45 ++++++ eadk_discord/fmt.py | 5 + 4 files changed, 314 insertions(+), 247 deletions(-) create mode 100644 eadk_discord/bot.py create mode 100644 eadk_discord/dates.py diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py new file mode 100644 index 0000000..e7738ab --- /dev/null +++ b/eadk_discord/bot.py @@ -0,0 +1,224 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Callable +from zoneinfo import ZoneInfo + +import discord +from discord.app_commands import AppCommandError +from pydantic import BaseModel, Field + +from eadk_discord import dates, fmt +from eadk_discord.database import Database +from eadk_discord.event import BookDesk, Event, MakeFlex, MakeOwned, UnbookDesk +from eadk_discord.state import HandleEventError + +TIME_ZONE = ZoneInfo("Europe/Copenhagen") + + +class CommandInfo(BaseModel): + now: datetime = Field() + format_user: Callable[[int], str] = Field() + author_id: int = Field() + + @staticmethod + def from_interaction(interaction: discord.Interaction) -> "CommandInfo": + return CommandInfo( + now=datetime.now(TIME_ZONE), + format_user=lambda user: fmt.user(interaction, user), + author_id=interaction.user.id, + ) + + +@dataclass +class Response: + message: str + ephemeral: bool + embed: discord.Embed | None + + def __init__(self, message: str = "", ephemeral: bool = False, embed: discord.Embed | None = None) -> None: + self.message = message + self.ephemeral = ephemeral + self.embed = embed + + async def send(self, interaction: discord.Interaction) -> None: + if self.embed is None: + await interaction.response.send_message(self.message, ephemeral=self.ephemeral) + else: + await interaction.response.send_message(self.message, ephemeral=self.ephemeral, embed=self.embed) + + +class EADKBot: + _database: Database + + def __init__(self, database: Database) -> None: + self._database = database + + def info(self, info: CommandInfo, date_str: str | None) -> Response: + booking_date = dates.get_booking_date(date_str, info.now) + booking_day, _ = self._database.state.day(booking_date) + + desk_numbers_str = "\n".join(str(i + 1) for i in range(len(booking_day.desks))) + desk_bookers_str = "\n".join( + info.format_user(desk.booker) if desk.booker else "**Free**" for desk in booking_day.desks + ) + desk_owners_str = "\n".join( + info.format_user(desk.owner) if desk.owner else "**Flex**" for desk in booking_day.desks + ) + + return Response( + message="", + ephemeral=True, + embed=discord.Embed(title="Desk availability", description=f"{booking_date.strftime('%A %Y-%m-%d')}") + .add_field(name="Desk", value=desk_numbers_str, inline=True) + .add_field(name="Booked by", value=desk_bookers_str, inline=True) + .add_field(name="Owner", value=desk_owners_str, inline=True), + ) + + def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_arg: int | None) -> Response: + if user_id is None: + user_id = info.author_id + + booking_date = dates.get_booking_date(date_str, info.now) + booking_day, _ = self._database.state.day(booking_date) + date_str = fmt.date(booking_date) + + if desk_arg: + if desk_arg < 1 or desk_arg > len(booking_day.desks): + return Response( + message=f"Desk {desk_arg} does not exist. There are only {len(booking_day.desks)} desks.", + ephemeral=True, + ) + desk_index = desk_arg - 1 + desk_num = desk_arg + else: + desk_index_option = booking_day.get_available_desk() + if desk_index_option is not None: + desk_index = desk_index_option + desk_num = desk_index + 1 + else: + return Response(message=f"No more desks are available for booking on {date_str}.", ephemeral=True) + self._database.handle_event( + Event( + author=info.author_id, + time=datetime.now(), + event=BookDesk(date=booking_date, desk_index=desk_index, user=user_id), + ) + ) + return Response(message=f"Desk {desk_num} has been booked for {info.format_user(user_id)} on {date_str}.") + + def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk: int | None) -> Response: + booking_date = dates.get_booking_date(date_str, info.now) + booking_day, _ = self._database.state.day(booking_date) + date_str = fmt.date(booking_date) + + if booking_date < info.now.date(): + return Response( + message=f"Date {date_str} not available for booking. Desks cannot be unbooked in the past.", + ephemeral=True, + ) + + if desk is not None: + if desk < 1 or desk > len(booking_day.desks): + return Response( + message=f"Desk {desk} does not exist. There are only {len(booking_day.desks)} desks.", + ephemeral=True, + ) + desk_index = desk - 1 + desk_num = desk + if user_id is not None: + if user_id != booking_day.desk(desk_index).booker: + return Response( + message=f"Desk {desk} is not booked by {info.format_user(user_id)} on {date_str}.", + ephemeral=True, + ) + else: + if user_id is None: + user_id = info.author_id + desk_indices = booking_day.booked_desks(user_id) + if desk_indices: + desk_index = desk_indices[0] + desk_num = desk_index + 1 + else: + return Response( + message=f"{info.format_user(user_id)} already has no desks booked for {date_str}.", ephemeral=True + ) + + desk_booker = booking_day.desk(desk_index).booker + if desk_booker: + self._database.handle_event( + Event( + author=info.author_id, + time=datetime.now(), + event=UnbookDesk(date=booking_date, desk_index=desk_index), + ) + ) + return Response( + message=f"Desk {desk_num} is no longer booked for {info.format_user(desk_booker)} on {date_str}." + ) + else: + return Response(message=f"Desk {desk_num} is already free on {date_str}.", ephemeral=True) + + def makeowned(self, info: CommandInfo, start_date_str: str, user_id: int | None, desk_num: int) -> Response: + booking_date = dates.get_booking_date(start_date_str, info.now) + date_str = fmt.date(booking_date) + + if booking_date < info.now.date(): + return Response( + message=f"Date {date_str} not available for booking. Desks cannot be made permanent retroactively.", + ephemeral=True, + ) + + desk_index = desk_num - 1 + + if user_id is None: + user_id = info.author_id + self._database.handle_event( + Event( + author=info.author_id, + time=datetime.now(), + event=MakeOwned(start_date=booking_date, desk_index=desk_index, user=user_id), + ) + ) + return Response(message=f"Desk {desk_num} is now owned by {info.format_user(user_id)} from {date_str} onwards.") + + def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Response: + booking_date = dates.get_booking_date(start_date_str, info.now) + date_str = fmt.date(booking_date) + + if booking_date < info.now.date(): + return Response( + message=f"Date {date_str} not available for booking. You cannot make a desk permanent retroactively.", + ephemeral=True, + ) + + desk_index = desk_num - 1 + + self._database.handle_event( + Event( + author=info.author_id, + time=datetime.now(), + event=MakeFlex(start_date=booking_date, desk_index=desk_index), + ) + ) + return Response(message=f"Desk {desk_num} is now a flex desk from {date_str} onwards.") + + def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: + if isinstance(error, discord.app_commands.errors.MissingAnyRole) or isinstance( + error, discord.app_commands.errors.MissingRole + ): + return Response(message="You do not have permission to run this command.", ephemeral=True) + if isinstance(error, discord.app_commands.errors.CheckFailure): + return Response(message="This command can only be used in the office channel.", ephemeral=True) + if isinstance(error, discord.app_commands.errors.TransformerError): + match error.__cause__: + case dates.DateParseError(arg): + return Response( + message=f"Date {arg} could not be parsed. " + "Please use the format YYYY-MM-DD, 'today', 'tomorrow', or specify a weekday.", + ephemeral=True, + ) + if isinstance(error, discord.app_commands.errors.CommandInvokeError): + match error.__cause__: + case HandleEventError(_event, event_error): + return Response(message=event_error.message(info.format_user), ephemeral=True) + raise error diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index f633b32..dfa3d7d 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -1,45 +1,23 @@ import logging -from datetime import date, datetime, timedelta +from datetime import date, datetime from pathlib import Path -from zoneinfo import ZoneInfo import discord from discord import Intents, Interaction, Member, app_commands from discord.abc import Snowflake -from discord.app_commands import AppCommandError, Choice, Range, Transform +from discord.app_commands import AppCommandError, Choice, Range from discord.ext import commands from discord.ext.commands import Bot, Context -from eadk_discord import fmt +from eadk_discord.bot import CommandInfo, EADKBot, Response from eadk_discord.database import Database -from eadk_discord.date_converter import DateConverter, DateParseError -from eadk_discord.event import BookDesk, Event, MakeFlex, MakeOwned, SetNumDesks, UnbookDesk -from eadk_discord.state import HandleEventError - -TIME_ZONE = ZoneInfo("Europe/Copenhagen") +from eadk_discord.event import Event, SetNumDesks TEST_SERVER_ROLE_ID = ***REMOVED*** EADK_DESK_ADMIN_ID = ***REMOVED*** EADK_DESK_REGULAR_ID = ***REMOVED*** -def format_date(date: date) -> str: - return date.isoformat() - - -def get_booking_date(booking_date_arg: date | None) -> date: - """ - Returns a tuple of two values: - - A boolean indicating whether it is currently before 17:00. - - A date object representing the date on which desks should be booked. - """ - if booking_date_arg is not None: - return booking_date_arg - now = datetime.now(TIME_ZONE) - booking_date = now.date() if now.hour < 17 else now.date() + timedelta(days=1) - return booking_date - - def author_id(interaction: Interaction) -> int: return interaction.user.id @@ -69,245 +47,84 @@ def setup_bot(database_path: Path, guilds: list[Snowflake]) -> Bot: intents.message_content = True intents.members = True + eadk_bot = EADKBot(database) bot = Bot(command_prefix="!", intents=intents) @bot.tree.command(name="info", description="Get current booking status.", guilds=guilds) - @app_commands.autocomplete(booking_date_arg=date_autocomplete) - @app_commands.rename(booking_date_arg="date") + @app_commands.autocomplete(date_arg=date_autocomplete) + @app_commands.rename(date_arg="date") @app_commands.check(channel_check) async def info( interaction: Interaction, - booking_date_arg: Transform[date, DateConverter] | None, + date_arg: str | None, ) -> None: - booking_date = get_booking_date(booking_date_arg) - booking_day, _ = database.state.day(booking_date) - - desk_numbers_str = "\n".join(str(i + 1) for i in range(len(booking_day.desks))) - desk_bookers_str = "\n".join( - fmt.user(interaction, desk.booker) if desk.booker else "**Free**" for desk in booking_day.desks - ) - desk_owners_str = "\n".join( - fmt.user(interaction, desk.owner) if desk.owner else "**Flex**" for desk in booking_day.desks - ) - - await interaction.response.send_message( - embed=discord.Embed(title="Desk availability", description=f"{booking_date.strftime('%A %Y-%m-%d')}") - .add_field(name="Desk", value=desk_numbers_str, inline=True) - .add_field(name="Booked by", value=desk_bookers_str, inline=True) - .add_field(name="Owner", value=desk_owners_str, inline=True), - ephemeral=True, - ) + await eadk_bot.info( + CommandInfo.from_interaction(interaction), + date_arg, + ).send(interaction) @bot.tree.command(name="book", description="Book a desk.", guilds=guilds) @app_commands.autocomplete(booking_date_arg=date_autocomplete) - @app_commands.rename(booking_date_arg="date") - @app_commands.rename(desk="desk_id") + @app_commands.rename(booking_date_arg="date", desk="desk_id") @app_commands.check(channel_check) @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID, EADK_DESK_REGULAR_ID) async def book( interaction: Interaction, - booking_date_arg: Transform[date, DateConverter] | None, + booking_date_arg: str | None, user: Member | None, desk: Range[int, 1] | None, ) -> None: - booking_date = get_booking_date(booking_date_arg) - booking_day, _ = database.state.day(booking_date) - date_str = format_date(booking_date) - - if booking_date < date.today(): - await interaction.response.send_message( - f"Date {date_str} not available for booking. Desks cannot be booked in the past.", ephemeral=True - ) - return - - if user: - user_id = user.id - else: - user_id = author_id(interaction) - - if desk: - if desk < 1 or desk > len(booking_day.desks): - await interaction.response.send_message( - f"Desk {desk} does not exist. There are only {len(booking_day.desks)} desks.", ephemeral=True - ) - return - desk_index = desk - 1 - desk_num = desk - else: - desk_index_option = booking_day.get_available_desk() - if desk_index_option is not None: - desk_index = desk_index_option - desk_num = desk_index + 1 - else: - await interaction.response.send_message( - f"No more desks are available for booking on {date_str}.", ephemeral=True - ) - return - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=BookDesk(date=booking_date, desk_index=desk_index, user=user_id), - ) - ) - await interaction.response.send_message( - f"Desk {desk_num} has been booked for {fmt.user(interaction, user_id)} on {date_str}." - ) + await eadk_bot.book( + CommandInfo.from_interaction(interaction), + booking_date_arg, + user.id if user else None, + desk, + ).send(interaction) database.save(database_path) @bot.tree.command(name="unbook", description="Unbook a desk.", guilds=guilds) @app_commands.autocomplete(booking_date_arg=date_autocomplete) - @app_commands.rename(booking_date_arg="date") - @app_commands.rename(desk="desk_id") + @app_commands.rename(booking_date_arg="date", desk="desk_id") @app_commands.check(channel_check) @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID, EADK_DESK_REGULAR_ID) async def unbook( interaction: Interaction, - booking_date_arg: Transform[date, DateConverter] | None, + booking_date_arg: str | None, user: Member | None, desk: Range[int, 1] | None, ) -> None: - booking_date = get_booking_date(booking_date_arg) - booking_day, _ = database.state.day(booking_date) - date_str = format_date(booking_date) - - if booking_date < date.today(): - await interaction.response.send_message( - f"Date {date_str} not available for booking. Desks cannot be unbooked in the past.", ephemeral=True - ) - return - - if desk is not None: - if desk < 1 or desk > len(booking_day.desks): - await interaction.response.send_message( - f"Desk {desk} does not exist. There are only {len(booking_day.desks)} desks.", ephemeral=True - ) - return - desk_index = desk - 1 - desk_num = desk - if user is not None: - if user.id != booking_day.desk(desk_index).booker: - await interaction.response.send_message( - f"Desk {desk} is not booked by {fmt.user(interaction, user.id)} on {date_str}.", - ephemeral=True, - ) - return - else: - if user: - user_id = user.id - else: - user_id = author_id(interaction) - desk_indices = booking_day.booked_desks(user_id) - if desk_indices: - desk_index = desk_indices[0] - desk_num = desk_index + 1 - else: - await interaction.response.send_message( - f"{fmt.user(interaction, user_id)} already has no desks booked for {date_str}.", ephemeral=True - ) - return - - desk_booker = booking_day.desk(desk_index).booker - if desk_booker: - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=UnbookDesk(date=booking_date, desk_index=desk_index), - ) - ) - await interaction.response.send_message( - f"Desk {desk_num} is no longer booked for {fmt.user(interaction, desk_booker)} on {date_str}." - ) - else: - await interaction.response.send_message(f"Desk {desk_num} is already free on {date_str}.", ephemeral=True) + await eadk_bot.unbook( + CommandInfo.from_interaction(interaction), booking_date_arg, user.id if user else None, desk + ).send(interaction) database.save(database_path) @bot.tree.command( name="makeowned", description="Make a user the owner of the desk from a specific date onwards", guilds=guilds ) - @app_commands.autocomplete(start_date=date_autocomplete) - @app_commands.rename(desk="desk_id") + @app_commands.autocomplete(start_date_str=date_autocomplete) + @app_commands.rename(start_date_str="start_date", desk="desk_id") @app_commands.check(channel_check) @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID) async def makeowned( interaction: Interaction, - start_date: Transform[date, DateConverter], + start_date_str: str, user: Member | None, desk: Range[int, 1], ) -> None: - booking_date = get_booking_date(start_date) - booking_day, _ = database.state.day(booking_date) - date_str = format_date(booking_date) - - if booking_date < date.today(): - await interaction.response.send_message( - f"Date {date_str} not available for booking. Desks cannot be made permanent retroactively.", - ephemeral=True, - ) - return - - if desk < 1 or desk > len(booking_day.desks): - await interaction.response.send_message( - f"Desk {desk} does not exist. There are only {len(booking_day.desks)} desks.", ephemeral=True - ) - return - - desk_index = desk - 1 - - if user: - user_id = user.id - else: - user_id = author_id(interaction) - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=MakeOwned(start_date=booking_date, desk_index=desk_index, user=user_id), - ) - ) - await interaction.response.send_message( - f"Desk {desk} is now owned by {fmt.user(interaction, user_id)} from {date_str} onwards." - ) + await eadk_bot.makeowned( + CommandInfo.from_interaction(interaction), start_date_str, user.id if user else None, desk + ).send(interaction) database.save(database_path) @bot.tree.command( name="makeflex", description="Make a desk a flex desk from a specific date onwards", guilds=guilds ) - @app_commands.autocomplete(start_date=date_autocomplete) - @app_commands.rename(desk="desk_id") + @app_commands.autocomplete(start_date_str=date_autocomplete) + @app_commands.rename(start_date_str="start_date", desk="desk_id") @app_commands.check(channel_check) @app_commands.checks.has_any_role(TEST_SERVER_ROLE_ID, EADK_DESK_ADMIN_ID) - async def makeflex( - interaction: Interaction, start_date: Transform[date, DateConverter], desk: Range[int, 1] - ) -> None: - booking_date = get_booking_date(start_date) - booking_day, _ = database.state.day(booking_date) - date_str = format_date(booking_date) - - if booking_date < date.today(): - await interaction.response.send_message( - f"Date {date_str} not available for booking. You cannot make a desk permanent retroactively.", - ephemeral=True, - ) - return - - if desk < 1 or desk > len(booking_day.desks): - await interaction.response.send_message( - f"Desk {desk} does not exist. There are only {len(booking_day.desks)} desks.", ephemeral=True - ) - return - - desk_index = desk - 1 - - database.handle_event( - Event( - author=author_id(interaction), - time=datetime.now(), - event=MakeFlex(start_date=booking_date, desk_index=desk_index), - ) - ) - await interaction.response.send_message(f"Desk {desk} is now a flex desk from {date_str} onwards.") + async def makeflex(interaction: Interaction, start_date_str: str, desk: Range[int, 1]) -> None: + await eadk_bot.makeflex(CommandInfo.from_interaction(interaction), start_date_str, desk).send(interaction) database.save(database_path) @bot.command() @@ -331,34 +148,10 @@ async def on_ready() -> None: @bot.tree.error async def on_error(interaction: Interaction, error: AppCommandError) -> None: - if isinstance(error, discord.app_commands.errors.MissingAnyRole) or isinstance( - error, discord.app_commands.errors.MissingRole - ): - await interaction.response.send_message("You do not have permission to run this command.", ephemeral=True) - return - if isinstance(error, discord.app_commands.errors.CheckFailure): - await interaction.response.send_message( - "This command can only be used in the office channel.", ephemeral=True - ) - return - if isinstance(error, discord.app_commands.errors.TransformerError): - match error.__cause__: - case DateParseError(arg): - await interaction.response.send_message( - f"Date {arg} could not be parsed. " - "Please use the format YYYY-MM-DD, 'today', 'tomorrow', or specify a weekday.", - ephemeral=True, - ) - return - if isinstance(error, discord.app_commands.errors.CommandInvokeError): - match error.__cause__: - case HandleEventError(_event, event_error): - await interaction.response.send_message( - event_error.message(lambda id: fmt.user(interaction, id)), ephemeral=True - ) - return - else: - await interaction.response.send_message("INTERNAL ERROR HAS OCCURRED BEEP BOOP", ephemeral=True) - raise error + try: + await eadk_bot.handle_error(CommandInfo.from_interaction(interaction), error).send(interaction) + except Exception: + await Response(message="INTERNAL ERROR HAS OCCURRED BEEP BOOP", ephemeral=True).send(interaction) + raise return bot diff --git a/eadk_discord/dates.py b/eadk_discord/dates.py new file mode 100644 index 0000000..f312591 --- /dev/null +++ b/eadk_discord/dates.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from datetime import date, datetime, timedelta + +WEEKDAYS = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +] + + +@dataclass +class DateParseError(Exception): + argument: str + + +def parse_date_arg(argument: str, today: date) -> date: + if argument.lower() == "today": + return today + elif argument.lower() == "tomorrow": + return today + timedelta(days=1) + elif argument.lower() in WEEKDAYS: + weekday = WEEKDAYS.index(argument.lower()) + return today + timedelta(days=(weekday - today.weekday()) % 7) + # Try to parse the argument as an integer representing the day of the month + else: + try: + return date.fromisoformat(argument) + except Exception: + raise DateParseError(argument) from Exception + + +def get_booking_date(booking_date_arg: str | None, now: datetime) -> date: + """ + Returns a tuple of two values: + - A boolean indicating whether it is currently before 17:00. + - A date object representing the date on which desks should be booked. + """ + if booking_date_arg is not None: + return parse_date_arg(booking_date_arg, now.date()) + booking_date = now.date() if now.hour < 17 else now.date() + timedelta(days=1) + return booking_date diff --git a/eadk_discord/fmt.py b/eadk_discord/fmt.py index ad7c9aa..330ec89 100644 --- a/eadk_discord/fmt.py +++ b/eadk_discord/fmt.py @@ -1,4 +1,5 @@ import logging +from datetime import date as Date # noqa: N812 from discord import Interaction @@ -7,6 +8,10 @@ def desk_index(index: int) -> str: return f"{index + 1}" +def date(date: Date) -> str: + return date.isoformat() + + def user(interaction: Interaction, user: int) -> str: match interaction.guild: case None: From 2efa1c80b94ef18bf67111acf60ae3155ea45766 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Tue, 1 Oct 2024 22:41:08 +0200 Subject: [PATCH 05/51] :fire: Remove unnecessary checks --- eadk_discord/bot.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index e7738ab..c235807 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -83,11 +83,6 @@ def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, des date_str = fmt.date(booking_date) if desk_arg: - if desk_arg < 1 or desk_arg > len(booking_day.desks): - return Response( - message=f"Desk {desk_arg} does not exist. There are only {len(booking_day.desks)} desks.", - ephemeral=True, - ) desk_index = desk_arg - 1 desk_num = desk_arg else: @@ -118,11 +113,6 @@ def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, d ) if desk is not None: - if desk < 1 or desk > len(booking_day.desks): - return Response( - message=f"Desk {desk} does not exist. There are only {len(booking_day.desks)} desks.", - ephemeral=True, - ) desk_index = desk - 1 desk_num = desk if user_id is not None: From b071c8fd911a88ebef0df4f61213eb94a1c6427b Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Tue, 1 Oct 2024 22:50:59 +0200 Subject: [PATCH 06/51] :sparkles: Prevent booking in the past --- eadk_discord/bot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index c235807..30b7b94 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -82,6 +82,12 @@ def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, des booking_day, _ = self._database.state.day(booking_date) date_str = fmt.date(booking_date) + if booking_date < info.now.date(): + return Response( + message=f"Date {date_str} not available for booking. Desks cannot be unbooked in the past.", + ephemeral=True, + ) + if desk_arg: desk_index = desk_arg - 1 desk_num = desk_arg From 43557ac4ed76ec3e9408b80da98e6196d8ffd1fd Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Wed, 2 Oct 2024 16:36:08 +0200 Subject: [PATCH 07/51] :rotating_light: Use `beartype` typehints --- eadk_discord/bot.py | 2 +- eadk_discord/history.py | 2 +- eadk_discord/state.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 30b7b94..5ec2e31 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -1,9 +1,9 @@ from dataclasses import dataclass from datetime import datetime -from typing import Callable from zoneinfo import ZoneInfo import discord +from beartype.typing import Callable from discord.app_commands import AppCommandError from pydantic import BaseModel, Field diff --git a/eadk_discord/history.py b/eadk_discord/history.py index 5f88ba1..7035fa8 100644 --- a/eadk_discord/history.py +++ b/eadk_discord/history.py @@ -1,6 +1,6 @@ from datetime import date as Date # noqa: N812 -from typing import Any # noqa: N812 +from beartype.typing import Any # noqa: N812 from pydantic import BaseModel, Field from .event import Event diff --git a/eadk_discord/state.py b/eadk_discord/state.py index b95cee1..8565113 100644 --- a/eadk_discord/state.py +++ b/eadk_discord/state.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from datetime import date as Date # noqa: N812 from datetime import timedelta as TimeDelta # noqa: N812 -from typing import Callable, Sequence # noqa: N812 +from beartype.typing import Callable, Sequence # noqa: N812 from pydantic import BaseModel, Field from .event import BookDesk, Event, MakeFlex, MakeOwned, SetNumDesks, UnbookDesk @@ -62,8 +62,8 @@ def _make_flex(self) -> bool: class Day(BaseModel): - date: Date = Field(serialization_alias="date") - desks: Sequence[DeskStatus] = Field(serialization_alias="desks") + date: Date = Field() + desks: Sequence[DeskStatus] = Field() @classmethod def create_unbooked(cls, date: Date, num_desks: int) -> "Day": @@ -84,7 +84,8 @@ def desk(self, desk: int) -> DeskStatus: raise ValueError("desk number must be non-negative") elif desk >= len(self.desks): raise ValueError("desk number is out of range. There are only {len(self.desks)} desks.") - return self.desks[desk] + result: DeskStatus = self.desks[desk] + return result def get_available_desk(self) -> int | None: """ From bb59d400ce0ffe91ec5c57efba4426462bb4ab1c Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Wed, 2 Oct 2024 16:36:42 +0200 Subject: [PATCH 08/51] :white_check_mark: Add basic test --- requirements_dev.txt | 1 + tests/test_commands.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/test_commands.py diff --git a/requirements_dev.txt b/requirements_dev.txt index 0127a35..513488f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ ruff ~= 0.6.5 mypy ~= 1.11.2 pre-commit ~= 3.8.0 +pytest ~= 8.3.3 diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..966927a --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta + +import pytest + +from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.database import Database +from eadk_discord.event import Event, SetNumDesks +from eadk_discord.state import DateTooEarlyError + + +def test() -> None: + now = datetime.fromisoformat("2024-09-14") # Saturday + today = now.date() + + database = Database.initialize(now) + database.handle_event(Event(author=None, time=now, event=SetNumDesks(date=today, num_desks=6))) + + bot = EADKBot(database) + + with pytest.raises(DateTooEarlyError): + bot.book( + CommandInfo(now=now, format_user=lambda user: str(user), author_id=1), + (today - timedelta(1)).isoformat(), + user_id=0, + desk_arg=1, + ) + + response = bot.book( + CommandInfo(now=now, format_user=lambda user: str(user), author_id=1), today.isoformat(), user_id=3, desk_arg=2 + ) + assert response.ephemeral is False + assert database.state.day(today)[0].desk(0).booker is None + assert database.state.day(today)[0].desk(1).booker == 3 From 91b074294ab738794891c5bbfc0f12a07dc49ec1 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 12:56:27 +0200 Subject: [PATCH 09/51] :sparkles: Add beartype annotations everywhere --- .pre-commit-config.yaml | 1 + eadk_discord/bot.py | 10 ++++++++++ eadk_discord/bot_setup.py | 2 ++ eadk_discord/database.py | 5 +++++ eadk_discord/dates.py | 4 ++++ eadk_discord/fmt.py | 4 ++++ eadk_discord/history.py | 4 ++++ eadk_discord/state.py | 19 +++++++++++++++++++ requirements.txt | 2 +- 9 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae6a809..06107ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,3 +53,4 @@ repos: - types-python-dateutil~=2.9.0 - discord.py~=2.4.0 - pydantic~=2.9.1 + - beartype~=0.19.0 diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 5ec2e31..8873d02 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -3,6 +3,7 @@ from zoneinfo import ZoneInfo import discord +from beartype import beartype from beartype.typing import Callable from discord.app_commands import AppCommandError from pydantic import BaseModel, Field @@ -20,6 +21,7 @@ class CommandInfo(BaseModel): format_user: Callable[[int], str] = Field() author_id: int = Field() + @beartype @staticmethod def from_interaction(interaction: discord.Interaction) -> "CommandInfo": return CommandInfo( @@ -35,11 +37,13 @@ class Response: ephemeral: bool embed: discord.Embed | None + @beartype def __init__(self, message: str = "", ephemeral: bool = False, embed: discord.Embed | None = None) -> None: self.message = message self.ephemeral = ephemeral self.embed = embed + @beartype async def send(self, interaction: discord.Interaction) -> None: if self.embed is None: await interaction.response.send_message(self.message, ephemeral=self.ephemeral) @@ -50,6 +54,7 @@ async def send(self, interaction: discord.Interaction) -> None: class EADKBot: _database: Database + @beartype def __init__(self, database: Database) -> None: self._database = database @@ -74,6 +79,7 @@ def info(self, info: CommandInfo, date_str: str | None) -> Response: .add_field(name="Owner", value=desk_owners_str, inline=True), ) + @beartype def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_arg: int | None) -> Response: if user_id is None: user_id = info.author_id @@ -107,6 +113,7 @@ def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, des ) return Response(message=f"Desk {desk_num} has been booked for {info.format_user(user_id)} on {date_str}.") + @beartype def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk: int | None) -> Response: booking_date = dates.get_booking_date(date_str, info.now) booking_day, _ = self._database.state.day(booking_date) @@ -154,6 +161,7 @@ def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, d else: return Response(message=f"Desk {desk_num} is already free on {date_str}.", ephemeral=True) + @beartype def makeowned(self, info: CommandInfo, start_date_str: str, user_id: int | None, desk_num: int) -> Response: booking_date = dates.get_booking_date(start_date_str, info.now) date_str = fmt.date(booking_date) @@ -177,6 +185,7 @@ def makeowned(self, info: CommandInfo, start_date_str: str, user_id: int | None, ) return Response(message=f"Desk {desk_num} is now owned by {info.format_user(user_id)} from {date_str} onwards.") + @beartype def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Response: booking_date = dates.get_booking_date(start_date_str, info.now) date_str = fmt.date(booking_date) @@ -198,6 +207,7 @@ def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Res ) return Response(message=f"Desk {desk_num} is now a flex desk from {date_str} onwards.") + @beartype def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: if isinstance(error, discord.app_commands.errors.MissingAnyRole) or isinstance( error, discord.app_commands.errors.MissingRole diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index dfa3d7d..d90a239 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -3,6 +3,7 @@ from pathlib import Path import discord +from beartype import beartype from discord import Intents, Interaction, Member, app_commands from discord.abc import Snowflake from discord.app_commands import AppCommandError, Choice, Range @@ -33,6 +34,7 @@ async def channel_check(interaction: Interaction[discord.Client]) -> bool: return interaction.channel_id == test_server_channel_id or interaction.channel_id == eadk_office_channel_id +@beartype def setup_bot(database_path: Path, guilds: list[Snowflake]) -> Bot: if database_path.exists(): database = Database.load(database_path) diff --git a/eadk_discord/database.py b/eadk_discord/database.py index b7c19b4..d94ee96 100644 --- a/eadk_discord/database.py +++ b/eadk_discord/database.py @@ -1,6 +1,7 @@ from datetime import date as Date # noqa: N812 from pathlib import Path +from beartype import beartype from pydantic import BaseModel, Field from eadk_discord.history import History @@ -13,16 +14,19 @@ class Database(BaseModel): history: History = Field() state: State = Field() + @beartype @staticmethod def initialize(start_date: Date) -> "Database": history = History.initialize(start_date) state = State.initialize(history) return Database(history=history, state=state) + @beartype def save(self, path: Path) -> None: with path.open("w") as file: file.write(self.history.to_json()) + @beartype @staticmethod def load(path: Path) -> "Database": with path.open("r") as file: @@ -31,6 +35,7 @@ def load(path: Path) -> "Database": state = State.initialize(history) return Database(history=history, state=state) + @beartype def handle_event(self, event: Event) -> None: self.state.handle_event(event) self.history.append(event) diff --git a/eadk_discord/dates.py b/eadk_discord/dates.py index f312591..d6ded2f 100644 --- a/eadk_discord/dates.py +++ b/eadk_discord/dates.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta +from beartype import beartype + WEEKDAYS = [ "monday", "tuesday", @@ -17,6 +19,7 @@ class DateParseError(Exception): argument: str +@beartype def parse_date_arg(argument: str, today: date) -> date: if argument.lower() == "today": return today @@ -33,6 +36,7 @@ def parse_date_arg(argument: str, today: date) -> date: raise DateParseError(argument) from Exception +@beartype def get_booking_date(booking_date_arg: str | None, now: datetime) -> date: """ Returns a tuple of two values: diff --git a/eadk_discord/fmt.py b/eadk_discord/fmt.py index 330ec89..cfb6cf9 100644 --- a/eadk_discord/fmt.py +++ b/eadk_discord/fmt.py @@ -1,17 +1,21 @@ import logging from datetime import date as Date # noqa: N812 +from beartype import beartype from discord import Interaction +@beartype def desk_index(index: int) -> str: return f"{index + 1}" +@beartype def date(date: Date) -> str: return date.isoformat() +@beartype def user(interaction: Interaction, user: int) -> str: match interaction.guild: case None: diff --git a/eadk_discord/history.py b/eadk_discord/history.py index 7035fa8..5f4fbc2 100644 --- a/eadk_discord/history.py +++ b/eadk_discord/history.py @@ -1,5 +1,6 @@ from datetime import date as Date # noqa: N812 +from beartype import beartype from beartype.typing import Any # noqa: N812 from pydantic import BaseModel, Field @@ -20,13 +21,16 @@ def to_dict(self) -> dict[str, Any]: def to_json(self) -> str: return self.model_dump_json() + @beartype @staticmethod def from_json(data: str) -> "History": return History.model_validate_json(data) + @beartype @staticmethod def from_dict(data: dict[Any, Any]) -> "History": return History.model_validate(data) + @beartype def append(self, event: Event) -> None: self.history.append(event) diff --git a/eadk_discord/state.py b/eadk_discord/state.py index 8565113..2637c25 100644 --- a/eadk_discord/state.py +++ b/eadk_discord/state.py @@ -3,6 +3,7 @@ from datetime import date as Date # noqa: N812 from datetime import timedelta as TimeDelta # noqa: N812 +from beartype import beartype from beartype.typing import Callable, Sequence # noqa: N812 from pydantic import BaseModel, Field @@ -16,6 +17,7 @@ class DeskStatus(BaseModel): booker: int | None = Field(serialization_alias="booker") owner: int | None = Field(serialization_alias="owner") + @beartype def _book(self, user: int) -> bool: """ Returns True if the desk was successfully booked, False if the desk was already booked. @@ -26,6 +28,7 @@ def _book(self, user: int) -> bool: self.booker = user return True + @beartype def _unbook(self) -> bool: """ Returns True if the desk was successfully unbooked, False if the desk was not booked. @@ -36,6 +39,7 @@ def _unbook(self) -> bool: else: return False + @beartype def _make_owned(self, user: int) -> bool: """ Returns True if the desk was successfully permanently booked, False if the desk was already permanently booked. @@ -48,6 +52,7 @@ def _make_owned(self, user: int) -> bool: self.owner = user return True + @beartype def _make_flex(self) -> bool: """ Returns True if the desk was successfully unpermanently booked, False if the desk was not permanently booked. @@ -65,10 +70,12 @@ class Day(BaseModel): date: Date = Field() desks: Sequence[DeskStatus] = Field() + @beartype @classmethod def create_unbooked(cls, date: Date, num_desks: int) -> "Day": return cls(date=date, desks=[DeskStatus(booker=None, owner=None) for _ in range(num_desks)]) + @beartype @classmethod def create_from_previous(cls, previous: "Day") -> "Day": return cls( @@ -76,6 +83,7 @@ def create_from_previous(cls, previous: "Day") -> "Day": desks=[DeskStatus(booker=desk.owner, owner=desk.owner) for desk in previous.desks], ) + @beartype def desk(self, desk: int) -> DeskStatus: """ Returns the DeskStatus object for the given desk. @@ -87,6 +95,7 @@ def desk(self, desk: int) -> DeskStatus: result: DeskStatus = self.desks[desk] return result + @beartype def get_available_desk(self) -> int | None: """ Returns the first available desk, or None if all desks are booked. @@ -96,12 +105,14 @@ def get_available_desk(self) -> int | None: return i return None + @beartype def available_desks(self) -> list[int]: """ Returns a list of indices of available desks. """ return [i for i, desk in enumerate(self.desks) if desk.booker is None] + @beartype def booked_desks(self, member: int) -> list[int]: """ Returns the index of the desk booked by the given member, or None if the member has not booked a desk. @@ -239,6 +250,7 @@ class State(BaseModel): start_date: Date = Field(serialization_alias="start_date") days: list[Day] = Field(serialization_alias="days") + @beartype @staticmethod def initialize(history: History) -> "State": state = State(start_date=history.start_date, days=[Day.create_unbooked(history.start_date, 0)]) @@ -246,6 +258,7 @@ def initialize(history: History) -> "State": state.handle_event(event) return state + @beartype def day(self, date: Date) -> tuple[Day, int]: """ Returns the Day object for the given date, or None if the date is not in the database. @@ -257,6 +270,7 @@ def day(self, date: Date) -> tuple[Day, int]: self.days.append(Day.create_from_previous(self.days[-1])) return self.days[day_index], day_index + @beartype def handle_event(self, event: Event) -> None: try: match event.event: @@ -273,6 +287,7 @@ def handle_event(self, event: Event) -> None: except EventError as e: raise HandleEventError(event=event, error=e) from e + @beartype def _set_num_desks(self, event: SetNumDesks) -> None: _, day_index = self.day(event.date) for day in self.days[day_index:]: @@ -291,6 +306,7 @@ def _set_num_desks(self, event: SetNumDesks) -> None: for day in self.days[day_index:]: day.desks = day.desks[: event.num_desks] + @beartype def _book_desk(self, event: BookDesk) -> None: day, _ = self.day(event.date) desk_index = event.desk_index @@ -301,6 +317,7 @@ def _book_desk(self, event: BookDesk) -> None: raise DeskAlreadyBookedError(booker=desk.booker, desk=desk_index, day=event.date) desk.booker = event.user + @beartype def _unbook_desk(self, event: UnbookDesk) -> None: day, _ = self.day(event.date) desk_index = event.desk_index @@ -311,6 +328,7 @@ def _unbook_desk(self, event: UnbookDesk) -> None: raise DeskNotBookedError(desk=desk_index, day=event.date) desk.booker = None + @beartype def _make_owned(self, event: MakeOwned) -> None: day, day_index = self.day(event.start_date) desk_index = event.desk_index @@ -327,6 +345,7 @@ def _make_owned(self, event: MakeOwned) -> None: break day.desks[desk_index]._make_owned(event.user) + @beartype def _make_flex(self, event: MakeFlex) -> None: day, day_index = self.day(event.start_date) desk_index = event.desk_index diff --git a/requirements.txt b/requirements.txt index d25fce8..8d6ba5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ discord.py ~= 2.4.0 pydantic ~= 2.9.1 -beartype ~= 0.18.5 +beartype ~= 0.19.0 From 2d6f220a043a2da1a8fe714080e91eb2e3c9fa39 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 15:30:51 +0200 Subject: [PATCH 10/51] :white_check_mark: Split tests --- tests/test_commands.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 966927a..7b8a72a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import pytest @@ -7,27 +7,37 @@ from eadk_discord.event import Event, SetNumDesks from eadk_discord.state import DateTooEarlyError +NOW: datetime = datetime.fromisoformat("2024-09-14") # Saturday +TODAY: date = NOW.date() -def test() -> None: - now = datetime.fromisoformat("2024-09-14") # Saturday - today = now.date() - database = Database.initialize(now) - database.handle_event(Event(author=None, time=now, event=SetNumDesks(date=today, num_desks=6))) +def setup() -> tuple[EADKBot, Database]: + database = Database.initialize(TODAY) + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=TODAY, num_desks=6))) bot = EADKBot(database) + return bot, database + + +def test_book() -> None: + bot, database = setup() + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=3, desk_arg=2 + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(0).booker is None + assert database.state.day(TODAY)[0].desk(1).booker == 3 + + +def test_book_too_early() -> None: + bot, _ = setup() + with pytest.raises(DateTooEarlyError): bot.book( - CommandInfo(now=now, format_user=lambda user: str(user), author_id=1), - (today - timedelta(1)).isoformat(), + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + (TODAY - timedelta(1)).isoformat(), user_id=0, desk_arg=1, ) - - response = bot.book( - CommandInfo(now=now, format_user=lambda user: str(user), author_id=1), today.isoformat(), user_id=3, desk_arg=2 - ) - assert response.ephemeral is False - assert database.state.day(today)[0].desk(0).booker is None - assert database.state.day(today)[0].desk(1).booker == 3 From 2a01ccda6746039fac22788bf65f45812ea4b384 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 15:37:45 +0200 Subject: [PATCH 11/51] :rotating_light: Ignore deprecation of transitive dependency --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0624c2d..567fefb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ requires-python = ">=3.11" version = "0.0.1" [tool.pytest.ini_options] +filterwarnings = [ + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning" +] pythonpath = ["eadk_discord"] [tool.ruff] From ccd3aa1daa0361b3867ae5c9f827bc75408bcde8 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 15:41:42 +0200 Subject: [PATCH 12/51] :bug: Add missing beartype annotation --- eadk_discord/bot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 8873d02..86395ff 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -58,6 +58,7 @@ class EADKBot: def __init__(self, database: Database) -> None: self._database = database + @beartype def info(self, info: CommandInfo, date_str: str | None) -> Response: booking_date = dates.get_booking_date(date_str, info.now) booking_day, _ = self._database.state.day(booking_date) From c2d18d45e6ab5b50621181bd1c9dee8e179fa941 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 15:42:11 +0200 Subject: [PATCH 13/51] :sparkles: Add database property to `EADKBot` --- eadk_discord/bot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 86395ff..75b06fd 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -58,6 +58,10 @@ class EADKBot: def __init__(self, database: Database) -> None: self._database = database + @property + def database(self) -> Database: + return self._database + @beartype def info(self, info: CommandInfo, date_str: str | None) -> Response: booking_date = dates.get_booking_date(date_str, info.now) From 3dfc1ee9c7955688f2c25418f5a6da30ef68371b Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 15:46:30 +0200 Subject: [PATCH 14/51] :sparkles: Turn test setup into fixture --- pyproject.toml | 1 + tests/test_commands.py | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 567fefb..e565d50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ requires-python = ">=3.11" version = "0.0.1" [tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" filterwarnings = [ "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning" ] diff --git a/tests/test_commands.py b/tests/test_commands.py index 7b8a72a..2b9e19c 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -11,17 +11,18 @@ TODAY: date = NOW.date() -def setup() -> tuple[EADKBot, Database]: +@pytest.fixture +def bot() -> EADKBot: database = Database.initialize(TODAY) database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=TODAY, num_desks=6))) bot = EADKBot(database) - return bot, database + return bot -def test_book() -> None: - bot, database = setup() +def test_book(bot: EADKBot) -> None: + database = bot.database response = bot.book( CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=3, desk_arg=2 @@ -31,9 +32,7 @@ def test_book() -> None: assert database.state.day(TODAY)[0].desk(1).booker == 3 -def test_book_too_early() -> None: - bot, _ = setup() - +def test_book_too_early(bot: EADKBot) -> None: with pytest.raises(DateTooEarlyError): bot.book( CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), From 2081b623095349d14161035dc49616d391ca8045 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 16:09:45 +0200 Subject: [PATCH 15/51] :technologist: Switch to UV --- eadk_discord/__main__.py | 2 +- pyproject.toml | 20 +- uv.lock | 617 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 631 insertions(+), 8 deletions(-) create mode 100644 uv.lock diff --git a/eadk_discord/__main__.py b/eadk_discord/__main__.py index 6eb60e2..8d4a4a9 100644 --- a/eadk_discord/__main__.py +++ b/eadk_discord/__main__.py @@ -5,7 +5,7 @@ import discord from discord.abc import Snowflake -from . import bot_setup +from eadk_discord import bot_setup if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) diff --git a/pyproject.toml b/pyproject.toml index e565d50..e3b40c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,13 @@ requires = ["setuptools", "setuptools-scm"] [project] authors = [{name = "albertsgarde", email = "albertsgarde@gmail.com"}] +dependencies = [ + "beartype>=0.19.0", + "discord-py>=2.4.0", + "pydantic>=2.9.2" +] description = "A Discord bot the EADK discord server" -dynamic = ["dependencies", "optional-dependencies"] +dynamic = ["optional-dependencies"] license = {file = "LICENSE"} name = "eadk_discord" readme = "README.md" @@ -13,7 +18,6 @@ requires-python = ">=3.11" version = "0.0.1" [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" filterwarnings = [ "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning" ] @@ -30,8 +34,10 @@ isort.known-first-party = ["eadk_discord"] [tool.setuptools] packages = ["eadk_discord"] -[tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} - -[tool.setuptools.dynamic.optional-dependencies] -dev = {file = ['requirements_dev.txt']} +[tool.uv] +dev-dependencies = [ + "mypy>=1.11.2", + "pre-commit>=3.8.0", + "pytest>=8.3.3", + "ruff>=0.6.8" +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ea96bd7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,617 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 }, +] + +[[package]] +name = "aiohttp" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/05/da5ff89c85444a6ade9079e73580fb3f78c6ba0e170a2472f15400d03e02/aiohttp-3.10.8.tar.gz", hash = "sha256:21f8225f7dc187018e8433c9326be01477fb2810721e048b33ac49091b19fb4a", size = 7540022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ca/2fc934c4c86865d0eb9c46f8f57443f0655f2a4a5c1dde60ec1d6d0f0881/aiohttp-3.10.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:33a68011a38020ed4ff41ae0dbf4a96a202562ecf2024bdd8f65385f1d07f6ef", size = 586333 }, + { url = "https://files.pythonhosted.org/packages/4a/07/7215d085dc10dd2e10f36832b2ca278f30970b4db98d5ebfed9e228d5c0c/aiohttp-3.10.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c7efa6616a95e3bd73b8a69691012d2ef1f95f9ea0189e42f338fae080c2fc6", size = 398817 }, + { url = "https://files.pythonhosted.org/packages/c4/e4/77b029c12d025d1e448662977f1e7c6fb33a19c42181c8d20c2791b5c5d9/aiohttp-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb9b9764cfb4459acf01c02d2a59d3e5066b06a846a364fd1749aa168efa2be", size = 390465 }, + { url = "https://files.pythonhosted.org/packages/17/f5/206e6a58a3a5be39662a07f531a6033384e361e272735437c5c15176c601/aiohttp-3.10.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7f270f4ca92760f98a42c45a58674fff488e23b144ec80b1cc6fa2effed377", size = 1306316 }, + { url = "https://files.pythonhosted.org/packages/33/e7/3b6b5ad02e367f30927bb93263127c23290f5b11900d036429f4787e1948/aiohttp-3.10.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6984dda9d79064361ab58d03f6c1e793ea845c6cfa89ffe1a7b9bb400dfd56bd", size = 1344486 }, + { url = "https://files.pythonhosted.org/packages/ae/9f/f27ba4cd2bffb4885aa35827a21878dbd3f50d6e5b205ce1107ce79edc40/aiohttp-3.10.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f6d47e392c27206701565c8df4cac6ebed28fdf6dcaea5b1eea7a4631d8e6db", size = 1378320 }, + { url = "https://files.pythonhosted.org/packages/54/76/b106eb516d327527a6b1e0409a3553745ad34480eddfd0d7cad48ddc9848/aiohttp-3.10.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a72f89aea712c619b2ca32c6f4335c77125ede27530ad9705f4f349357833695", size = 1292542 }, + { url = "https://files.pythonhosted.org/packages/7d/0c/c116a27253c0bc76959ab8df5a109d482c0977d4028e1b3ec7fac038bb1a/aiohttp-3.10.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36074b26f3263879ba8e4dbd33db2b79874a3392f403a70b772701363148b9f", size = 1251608 }, + { url = "https://files.pythonhosted.org/packages/9e/05/f9624dc401f72a3ee4cddea1a555b430e9a7be9d0cd2ab53dbec2fc78279/aiohttp-3.10.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e32148b4a745e70a255a1d44b5664de1f2e24fcefb98a75b60c83b9e260ddb5b", size = 1271551 }, + { url = "https://files.pythonhosted.org/packages/6d/77/19a032cfb9fdfd69591cf173c23c62992774b2ff978e4dab3038a1955e14/aiohttp-3.10.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5aa1a073514cf59c81ad49a4ed9b5d72b2433638cd53160fd2f3a9cfa94718db", size = 1266089 }, + { url = "https://files.pythonhosted.org/packages/12/63/58ebde5ea32cf5f19c83d6dc2c582ca5f0c42ce4cf084216a3cda4b2e34a/aiohttp-3.10.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3a79200a9d5e621c4623081ddb25380b713c8cf5233cd11c1aabad990bb9381", size = 1321455 }, + { url = "https://files.pythonhosted.org/packages/1a/22/d8439a280161b542a28f88794ab55917cdc672544b87db52d3c41ce8d9a1/aiohttp-3.10.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e45fdfcb2d5bcad83373e4808825b7512953146d147488114575780640665027", size = 1339057 }, + { url = "https://files.pythonhosted.org/packages/bc/67/1a76a69adfe3013863df4142d37059fb357146815b29596945d61fb940cb/aiohttp-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f78e2a78432c537ae876a93013b7bc0027ba5b93ad7b3463624c4b6906489332", size = 1298892 }, + { url = "https://files.pythonhosted.org/packages/38/13/7294cb679ab7a80e5b0d0aa97c527690cffed2f34cb8892d73ebdb4204e8/aiohttp-3.10.8-cp311-cp311-win32.whl", hash = "sha256:f8179855a4e4f3b931cb1764ec87673d3fbdcca2af496c8d30567d7b034a13db", size = 362066 }, + { url = "https://files.pythonhosted.org/packages/bc/4a/8881d4d7259427897e1a314c2724e65fd0d20084c72cac8360665f96c347/aiohttp-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:ef9b484604af05ca745b6108ca1aaa22ae1919037ae4f93aaf9a37ba42e0b835", size = 381406 }, + { url = "https://files.pythonhosted.org/packages/bb/ce/a8ff9f5bd2b36e3049cfe8d53656fed03075221ff42f946c581325bdc8fc/aiohttp-3.10.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ab2d6523575fc98896c80f49ac99e849c0b0e69cc80bf864eed6af2ae728a52b", size = 583366 }, + { url = "https://files.pythonhosted.org/packages/91/5c/75287ab8a6ae9cbe02d45ebb36b1e899c11da5eb47060e0dcb98ee30a951/aiohttp-3.10.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f5d5d5401744dda50b943d8764508d0e60cc2d3305ac1e6420935861a9d544bc", size = 395525 }, + { url = "https://files.pythonhosted.org/packages/a8/5a/aca17d71eb7e0f4611b2f28cb04e05aaebe6c7c2a7d1364e494da9722714/aiohttp-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de23085cf90911600ace512e909114385026b16324fa203cc74c81f21fd3276a", size = 390727 }, + { url = "https://files.pythonhosted.org/packages/1b/ee/c1663449864ec9dd3d2a61dde09112bea5e1d881496c36146a96fe85da62/aiohttp-3.10.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4618f0d2bf523043866a9ff8458900d8eb0a6d4018f251dae98e5f1fb699f3a8", size = 1311898 }, + { url = "https://files.pythonhosted.org/packages/8b/7e/ed2eb276fdf946a9303f3f80033555d3eaa0eadbcdd0c31b153e33b495fc/aiohttp-3.10.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21c1925541ca84f7b5e0df361c0a813a7d6a56d3b0030ebd4b220b8d232015f9", size = 1350380 }, + { url = "https://files.pythonhosted.org/packages/0c/3f/1d74a1311b14a1d69aad06775ffc1c09c195db67d951c8319220b9c64fdc/aiohttp-3.10.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:497a7d20caea8855c5429db3cdb829385467217d7feb86952a6107e033e031b9", size = 1392486 }, + { url = "https://files.pythonhosted.org/packages/9f/95/b940d71b1f61cf2ed48f2918c292609d251dba012a8e033afc0c778ed6a7/aiohttp-3.10.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c887019dbcb4af58a091a45ccf376fffe800b5531b45c1efccda4bedf87747ea", size = 1306135 }, + { url = "https://files.pythonhosted.org/packages/9b/25/b096aebc2f9b3ed738a4a667b841780b1dcd23ce5dff7dfabab4d09de4c8/aiohttp-3.10.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40d2d719c3c36a7a65ed26400e2b45b2d9ed7edf498f4df38b2ae130f25a0d01", size = 1260085 }, + { url = "https://files.pythonhosted.org/packages/9e/cf/bc024d8a848ee4feaae6a037034cf8b173a14ea9cb5c2988b6e5018abf33/aiohttp-3.10.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57359785f27394a8bcab0da6dcd46706d087dfebf59a8d0ad2e64a4bc2f6f94f", size = 1270968 }, + { url = "https://files.pythonhosted.org/packages/40/1d/2513347c445d1aaa694e79f4d45f80d777ea3e4d772d9480577834dc2c1c/aiohttp-3.10.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a961ee6f2cdd1a2be4735333ab284691180d40bad48f97bb598841bfcbfb94ec", size = 1280083 }, + { url = "https://files.pythonhosted.org/packages/22/e1/4be1b057044c3d874e795744446c682715b232281adbe94612ddc9877ee4/aiohttp-3.10.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe3d79d6af839ffa46fdc5d2cf34295390894471e9875050eafa584cb781508d", size = 1316638 }, + { url = "https://files.pythonhosted.org/packages/6d/c3/84492f103c724d3149bba413e1dc081e573c44013bd2cc8f4addd51cf365/aiohttp-3.10.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a281cba03bdaa341c70b7551b2256a88d45eead149f48b75a96d41128c240b3", size = 1343764 }, + { url = "https://files.pythonhosted.org/packages/cf/b7/50cc827dd54df087d7c30293b29fbc13a7ea45a3ac54a4a12127b271265c/aiohttp-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6769d71bfb1ed60321363a9bc05e94dcf05e38295ef41d46ac08919e5b00d19", size = 1306007 }, + { url = "https://files.pythonhosted.org/packages/1e/c0/a4cb21ad677757368743d73aff27047dfc0d7248cb39dec06c059b773c24/aiohttp-3.10.8-cp312-cp312-win32.whl", hash = "sha256:a3081246bab4d419697ee45e555cef5cd1def7ac193dff6f50be761d2e44f194", size = 359125 }, + { url = "https://files.pythonhosted.org/packages/d2/0f/1ecbc18eed29952393d5a9c4636bfe789dde3c98fe0a0a4759d323478e72/aiohttp-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:ab1546fc8e00676febc81c548a876c7bde32f881b8334b77f84719ab2c7d28dc", size = 379143 }, + { url = "https://files.pythonhosted.org/packages/9f/dd/3d944769ed65d3d245f8f976040654b3eae2e21d05c81f91fb450365bddf/aiohttp-3.10.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b1a012677b8e0a39e181e218de47d6741c5922202e3b0b65e412e2ce47c39337", size = 575934 }, + { url = "https://files.pythonhosted.org/packages/2a/bf/a6a1d14b0e5f90d53b1f0850204f9fafdfec7c1d99dda8aaea1dd93ba181/aiohttp-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2df786c96c57cd6b87156ba4c5f166af7b88f3fc05f9d592252fdc83d8615a3c", size = 391728 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/27cc6efa6ca3e563973c7e03e8b7e26b75b4046aefea991bad42c028a906/aiohttp-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8885ca09d3a9317219c0831276bfe26984b17b2c37b7bf70dd478d17092a4772", size = 387247 }, + { url = "https://files.pythonhosted.org/packages/ae/fd/235401bd4a98ea31cdda7b3822921e2a9cbc3ca0af1958a12a2709261735/aiohttp-3.10.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dbf252ac19860e0ab56cd480d2805498f47c5a2d04f5995d8d8a6effd04b48c", size = 1286909 }, + { url = "https://files.pythonhosted.org/packages/ab/1c/8ae6b12be2ae88e94be34d96765d6cc820d61d320f33c0423de8af0cfa47/aiohttp-3.10.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2036479b6b94afaaca7d07b8a68dc0e67b0caf5f6293bb6a5a1825f5923000", size = 1323446 }, + { url = "https://files.pythonhosted.org/packages/23/09/5ebe3a2dbdd008711b659dc2f2a6135bbc055b6c8869688083f4bec6b50a/aiohttp-3.10.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:365783e1b7c40b59ed4ce2b5a7491bae48f41cd2c30d52647a5b1ee8604c68ad", size = 1368237 }, + { url = "https://files.pythonhosted.org/packages/47/22/f184c27d03d34ce71e6d4b9976a4ff845d091b725f174b09f641e4a28f63/aiohttp-3.10.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270e653b5a4b557476a1ed40e6b6ce82f331aab669620d7c95c658ef976c9c5e", size = 1282598 }, + { url = "https://files.pythonhosted.org/packages/82/f6/bae1703bfacb19bb35e3522632fc5279793070625a0b5e567b109c0f0e8d/aiohttp-3.10.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8960fabc20bfe4fafb941067cda8e23c8c17c98c121aa31c7bf0cdab11b07842", size = 1236350 }, + { url = "https://files.pythonhosted.org/packages/a4/bc/ad73aced93836b8749c70e617c5d389d17a36da9ee220cdb0804f803bd9b/aiohttp-3.10.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f21e8f2abed9a44afc3d15bba22e0dfc71e5fa859bea916e42354c16102b036f", size = 1250172 }, + { url = "https://files.pythonhosted.org/packages/3b/18/027a8497caf3a9c247477831d67ede58e1e42a92fd635ecdb74cf5d45c8b/aiohttp-3.10.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fecd55e7418fabd297fd836e65cbd6371aa4035a264998a091bbf13f94d9c44d", size = 1248783 }, + { url = "https://files.pythonhosted.org/packages/6f/d2/5080c27b656e6d478e820752d633d7a4dab4a2c4fd23a6f645b553fb9da5/aiohttp-3.10.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:badb51d851358cd7535b647bb67af4854b64f3c85f0d089c737f75504d5910ec", size = 1293209 }, + { url = "https://files.pythonhosted.org/packages/ae/ec/c38c8690e804cb9bf3e8c473a4a7bb339ed549cd63c469f19995269ca9ec/aiohttp-3.10.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e860985f30f3a015979e63e7ba1a391526cdac1b22b7b332579df7867848e255", size = 1319943 }, + { url = "https://files.pythonhosted.org/packages/df/55/d6e3a13c3f37ad7a3e60a377c96541261c1943837d240f1ab2151a96da6b/aiohttp-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71462f8eeca477cbc0c9700a9464e3f75f59068aed5e9d4a521a103692da72dc", size = 1281380 }, + { url = "https://files.pythonhosted.org/packages/c3/31/0b84027487fa58a124251b47f9dca781e4777a50d1c4eea4d3fc8950bd10/aiohttp-3.10.8-cp313-cp313-win32.whl", hash = "sha256:177126e971782769b34933e94fddd1089cef0fe6b82fee8a885e539f5b0f0c6a", size = 357352 }, + { url = "https://files.pythonhosted.org/packages/cb/8a/b4f3a8d0fb7f4fdb3869db6c3334e23e11878123605579e067be85f7e01f/aiohttp-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:98a4eb60e27033dee9593814ca320ee8c199489fbc6b2699d0f710584db7feb7", size = 376618 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "beartype" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/e1/00515b97afa3993b4a314e4bc168fbde0917fd5845435cb6f16a19770746/beartype-0.19.0.tar.gz", hash = "sha256:de42dfc1ba5c3710fde6c3002e3bd2cad236ed4d2aabe876345ab0b4234a6573", size = 1294480 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/f6db6e4cb2fe2f887dead40b76caa91af4844cb647dd2c7223bb010aa416/beartype-0.19.0-py3-none-any.whl", hash = "sha256:33b2694eda0daf052eb2aff623ed9a8a586703bbf0a90bbc475a83bbf427f699", size = 1039760 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "discord-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/af/80cab4015722d3bee175509b7249a11d5adf77b5ff4c27f268558079d149/discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5", size = 1027707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/10/3c44e9331a5ec3bae8b2919d51f611a5b94e179563b1b89eb6423a8f43eb/discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d", size = 1125988 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "eadk-discord" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "beartype" }, + { name = "discord-py" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.19.0" }, + { name = "discord-py", specifier = ">=2.4.0" }, + { name = "pydantic", specifier = ">=2.9.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.11.2" }, + { name = "pre-commit", specifier = ">=3.8.0" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "ruff", specifier = ">=0.6.8" }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "frozenlist" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/3d/2102257e7acad73efc4a0c306ad3953f68c504c16982bbdfee3ad75d8085/frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", size = 37820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/bc/8d33f2d84b9368da83e69e42720cff01c5e199b5a868ba4486189a4d8fa9/frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", size = 97060 }, + { url = "https://files.pythonhosted.org/packages/af/b2/904500d6a162b98a70e510e743e7ea992241b4f9add2c8063bf666ca21df/frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", size = 55347 }, + { url = "https://files.pythonhosted.org/packages/5b/9c/f12b69997d3891ddc0d7895999a00b0c6a67f66f79498c0e30f27876435d/frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", size = 53374 }, + { url = "https://files.pythonhosted.org/packages/ac/6e/e0322317b7c600ba21dec224498c0c5959b2bce3865277a7c0badae340a9/frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", size = 273288 }, + { url = "https://files.pythonhosted.org/packages/a7/76/180ee1b021568dad5b35b7678616c24519af130ed3fa1e0f1ed4014e0f93/frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", size = 284737 }, + { url = "https://files.pythonhosted.org/packages/05/08/40159d706a6ed983c8aca51922a93fc69f3c27909e82c537dd4054032674/frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", size = 280267 }, + { url = "https://files.pythonhosted.org/packages/e0/18/9f09f84934c2b2aa37d539a322267939770362d5495f37783440ca9c1b74/frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", size = 258778 }, + { url = "https://files.pythonhosted.org/packages/b3/c9/0bc5ee7e1f5cc7358ab67da0b7dfe60fbd05c254cea5c6108e7d1ae28c63/frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", size = 272276 }, + { url = "https://files.pythonhosted.org/packages/12/5d/147556b73a53ad4df6da8bbb50715a66ac75c491fdedac3eca8b0b915345/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", size = 272424 }, + { url = "https://files.pythonhosted.org/packages/83/61/2087bbf24070b66090c0af922685f1d0596c24bb3f3b5223625bdeaf03ca/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", size = 260881 }, + { url = "https://files.pythonhosted.org/packages/a8/be/a235bc937dd803258a370fe21b5aa2dd3e7bfe0287a186a4bec30c6cccd6/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", size = 282327 }, + { url = "https://files.pythonhosted.org/packages/5d/e7/b2469e71f082948066b9382c7b908c22552cc705b960363c390d2e23f587/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74", size = 281502 }, + { url = "https://files.pythonhosted.org/packages/db/1b/6a5b970e55dffc1a7d0bb54f57b184b2a2a2ad0b7bca16a97ca26d73c5b5/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", size = 272292 }, + { url = "https://files.pythonhosted.org/packages/1a/05/ebad68130e6b6eb9b287dacad08ea357c33849c74550c015b355b75cc714/frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", size = 44446 }, + { url = "https://files.pythonhosted.org/packages/b3/21/c5aaffac47fd305d69df46cfbf118768cdf049a92ee6b0b5cb029d449dcf/frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", size = 50459 }, + { url = "https://files.pythonhosted.org/packages/b4/db/4cf37556a735bcdb2582f2c3fa286aefde2322f92d3141e087b8aeb27177/frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", size = 93937 }, + { url = "https://files.pythonhosted.org/packages/46/03/69eb64642ca8c05f30aa5931d6c55e50b43d0cd13256fdd01510a1f85221/frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", size = 53656 }, + { url = "https://files.pythonhosted.org/packages/3f/ab/c543c13824a615955f57e082c8a5ee122d2d5368e80084f2834e6f4feced/frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", size = 51868 }, + { url = "https://files.pythonhosted.org/packages/a9/b8/438cfd92be2a124da8259b13409224d9b19ef8f5a5b2507174fc7e7ea18f/frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", size = 280652 }, + { url = "https://files.pythonhosted.org/packages/54/72/716a955521b97a25d48315c6c3653f981041ce7a17ff79f701298195bca3/frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", size = 286739 }, + { url = "https://files.pythonhosted.org/packages/65/d8/934c08103637567084568e4d5b4219c1016c60b4d29353b1a5b3587827d6/frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", size = 289447 }, + { url = "https://files.pythonhosted.org/packages/70/bb/d3b98d83ec6ef88f9bd63d77104a305d68a146fd63a683569ea44c3085f6/frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", size = 265466 }, + { url = "https://files.pythonhosted.org/packages/0b/f2/b8158a0f06faefec33f4dff6345a575c18095a44e52d4f10c678c137d0e0/frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", size = 281530 }, + { url = "https://files.pythonhosted.org/packages/ea/a2/20882c251e61be653764038ece62029bfb34bd5b842724fff32a5b7a2894/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", size = 281295 }, + { url = "https://files.pythonhosted.org/packages/4c/f9/8894c05dc927af2a09663bdf31914d4fb5501653f240a5bbaf1e88cab1d3/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", size = 268054 }, + { url = "https://files.pythonhosted.org/packages/37/ff/a613e58452b60166507d731812f3be253eb1229808e59980f0405d1eafbf/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", size = 286904 }, + { url = "https://files.pythonhosted.org/packages/cc/6e/0091d785187f4c2020d5245796d04213f2261ad097e0c1cf35c44317d517/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", size = 290754 }, + { url = "https://files.pythonhosted.org/packages/a5/c2/e42ad54bae8bcffee22d1e12a8ee6c7717f7d5b5019261a8c861854f4776/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", size = 282602 }, + { url = "https://files.pythonhosted.org/packages/b6/61/56bad8cb94f0357c4bc134acc30822e90e203b5cb8ff82179947de90c17f/frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", size = 44063 }, + { url = "https://files.pythonhosted.org/packages/3e/dc/96647994a013bc72f3d453abab18340b7f5e222b7b7291e3697ca1fcfbd5/frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", size = 50452 }, + { url = "https://files.pythonhosted.org/packages/83/10/466fe96dae1bff622021ee687f68e5524d6392b0a2f80d05001cd3a451ba/frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", size = 11552 }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "mypy" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, + { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, + { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, + { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, + { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, + { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, + { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, + { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, + { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, + { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, + { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 }, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, +] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, + { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, + { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, + { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, + { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, + { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, + { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, + { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, + { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, + { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, + { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, + { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, + { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, + { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, + { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, + { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, + { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, + { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, + { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, + { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, + { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, + { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, + { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, + { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, + { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, + { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, + { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, + { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, + { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, + { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, + { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, + { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, + { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "ruff" +version = "0.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/f9/4ce3e765a72ab8fe0f80f48508ea38b4196daab3da14d803c21349b2d367/ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", size = 3084543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/07/42ee57e8b76ca585297a663a552b4f6d6a99372ca47fdc2276ef72cc0f2f/ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", size = 10404327 }, + { url = "https://files.pythonhosted.org/packages/eb/51/d42571ff8156d65086acb72d39aa64cb24181db53b497d0ed6293f43f07a/ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", size = 10018797 }, + { url = "https://files.pythonhosted.org/packages/c1/d7/fa5514a60b03976af972b67fe345deb0335dc96b9f9a9fa4df9890472427/ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", size = 9691303 }, + { url = "https://files.pythonhosted.org/packages/d6/c4/d812a74976927e51d0782a47539069657ac78535779bfa4d061c4fc8d89d/ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", size = 10719452 }, + { url = "https://files.pythonhosted.org/packages/ec/b6/aa700c4ae6db9b3ee660e23f3c7db596e2b16a3034b797704fba33ddbc96/ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", size = 10161353 }, + { url = "https://files.pythonhosted.org/packages/ea/39/0b10075ffcd52ff3a581b9b69eac53579deb230aad300ce8f9d0b58e77bc/ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", size = 10980630 }, + { url = "https://files.pythonhosted.org/packages/c1/af/9eb9efc98334f62652e2f9318f137b2667187851911fac3b395365a83708/ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", size = 11768996 }, + { url = "https://files.pythonhosted.org/packages/e0/59/8b1369cf7878358952b1c0a1559b4d6b5c824c003d09b0db26d26c9d094f/ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87", size = 11317469 }, + { url = "https://files.pythonhosted.org/packages/b9/6d/e252e9b11bbca4114c386ee41ad559d0dac13246201d77ea1223c6fea17f/ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", size = 12467185 }, + { url = "https://files.pythonhosted.org/packages/48/44/7caa223af7d4ea0f0b2bd34acca65a7694a58317714675a2478815ab3f45/ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", size = 10887766 }, + { url = "https://files.pythonhosted.org/packages/81/ed/394aff3a785f171869158b9d5be61eec9ffb823c3ad5d2bdf2e5f13cb029/ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", size = 10711609 }, + { url = "https://files.pythonhosted.org/packages/47/31/f31d04c842e54699eab7e3b864538fea26e6c94b71806cd10aa49f13e1c1/ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", size = 10237621 }, + { url = "https://files.pythonhosted.org/packages/20/95/a764e84acf11d425f2f23b8b78b4fd715e9c20be4aac157c6414ca859a67/ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", size = 10558329 }, + { url = "https://files.pythonhosted.org/packages/2a/76/d4e38846ac9f6dd62dce858a54583911361b5339dcf8f84419241efac93a/ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", size = 10954102 }, + { url = "https://files.pythonhosted.org/packages/e7/36/f18c678da6c69f8d022480f3e8ddce6e4a52e07602c1d212056fbd234f8f/ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", size = 8511090 }, + { url = "https://files.pythonhosted.org/packages/4c/c4/0ca7d8ffa358b109db7d7d045a1a076fd8e5d9cbeae022242d3c060931da/ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", size = 9350079 }, + { url = "https://files.pythonhosted.org/packages/d9/bd/a8b0c64945a92eaeeb8d0283f27a726a776a1c9d12734d990c5fc7a1278c/ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", size = 8669595 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +] + +[[package]] +name = "yarl" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/11/2b8334f4192646677a2e7da435670d043f536088af943ec242f31453e5ba/yarl-1.13.1.tar.gz", hash = "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0", size = 165912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/64/1eaa5d080ceb8742b75a25eff4d510439459ff9c7fbe03e8e929a732ca07/yarl-1.13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51", size = 189609 }, + { url = "https://files.pythonhosted.org/packages/e2/49/7faf592dd5d4ae4b789988750739c327b81070aa6d428848ce71f6112c1b/yarl-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e", size = 115504 }, + { url = "https://files.pythonhosted.org/packages/0c/02/6dd48672009bdf135a298a7250875321098b7cbbca5af8c49d8dae07b635/yarl-1.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc", size = 113754 }, + { url = "https://files.pythonhosted.org/packages/0e/4c/dd49a78833691ccdc15738eb814e37df47f0f25baeefb1cec64ecb4459eb/yarl-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495", size = 486101 }, + { url = "https://files.pythonhosted.org/packages/36/ec/e5e6ed4344de34d3554a22d181df4d90a4d0f257575c28b767ad8c1add0b/yarl-1.13.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2", size = 505989 }, + { url = "https://files.pythonhosted.org/packages/7d/af/0318b0d03471207b3959e0e6ca2964b689744d8482fdbfdc2958854373b4/yarl-1.13.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38", size = 500428 }, + { url = "https://files.pythonhosted.org/packages/c4/09/5e47823e3abb26ddda447b500be28137971d246b0c771a02f855dd06b30b/yarl-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74", size = 488954 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/e26317d48bd6bf59dfbb6049d022582a376de01440e5c2bbe92009f8117a/yarl-1.13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9", size = 471561 }, + { url = "https://files.pythonhosted.org/packages/93/c5/4dfb00b84fc6df79b3e42d8716ba8f747d7ebf0c14640c7e65d923f39ea7/yarl-1.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2", size = 485652 }, + { url = "https://files.pythonhosted.org/packages/9d/fb/bde1430c94d6e5de27d0031e3fb5d85467d975aecdc67e6c686f5c36bbfd/yarl-1.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f", size = 483530 }, + { url = "https://files.pythonhosted.org/packages/5c/80/9f9c9d567ac5fb355e252dc27b75ccf92a3e4bea8b1c5610d5d1240c1b30/yarl-1.13.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10", size = 514085 }, + { url = "https://files.pythonhosted.org/packages/aa/9b/3aeb817a60bde4be6acb476a46bc6184c27b5c91f23ec726d9e6e46b89cf/yarl-1.13.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da", size = 516342 }, + { url = "https://files.pythonhosted.org/packages/71/9d/d7aa4fd8b16e174c4c16b826f54a0e9e4533fb3ae09741906ccc811362d0/yarl-1.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246", size = 498430 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/b46aad1725f8d043beee2d47ffddffb1939178bec6f9584b46215efe5a78/yarl-1.13.1-cp311-cp311-win32.whl", hash = "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a", size = 102436 }, + { url = "https://files.pythonhosted.org/packages/89/9e/bbbda05279230dc12d879dfcf971f77f9c932e457fbcd870efb4c3bdf10c/yarl-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2", size = 111678 }, + { url = "https://files.pythonhosted.org/packages/64/de/1602352e5bb47c4b86921b004fe84d0646ef9abeda3dfc55f1d2271829e4/yarl-1.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9", size = 190253 }, + { url = "https://files.pythonhosted.org/packages/83/f0/2abc6f0af8f243c4a5190e687897e7684baea2c97f5f1be2321418163c7e/yarl-1.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe", size = 116079 }, + { url = "https://files.pythonhosted.org/packages/ad/eb/a578f935e2b6834a00b38156f81f3a6545e14a360ff8a296019116502a9c/yarl-1.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419", size = 113943 }, + { url = "https://files.pythonhosted.org/packages/da/ee/2bf5f8ffbea5b18fbca274dd04e300a033e43e92d261ac60722361b216ce/yarl-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57", size = 483984 }, + { url = "https://files.pythonhosted.org/packages/05/9f/20d07ed84cbac847b989ef61130f2cbec6dc60f273b81d51041c35740eb3/yarl-1.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b", size = 499723 }, + { url = "https://files.pythonhosted.org/packages/e5/90/cc6d3dab4fc33b6f80d498c6276995fcbe16db1005141be6133345b597c1/yarl-1.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c", size = 497279 }, + { url = "https://files.pythonhosted.org/packages/47/a0/c1404aa8c7e025aa05a81f3a34c42131f8b11836e49450e1558bcd64a3bb/yarl-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220", size = 490188 }, + { url = "https://files.pythonhosted.org/packages/2e/8b/ebb195c4a4a5b5a84b0ade8469404609d68adf8f1dcf88e8b2b5297566cc/yarl-1.13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8", size = 469378 }, + { url = "https://files.pythonhosted.org/packages/40/8f/6a00380c6653006ac0112ebbf0ff24eb7b2d71359ac2c410a98822d89bfa/yarl-1.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43", size = 485681 }, + { url = "https://files.pythonhosted.org/packages/2c/94/797d18a3b9ea125a24ba3c69cd71b3561d227d5bb61dbadf2cb2afd6c319/yarl-1.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4", size = 486049 }, + { url = "https://files.pythonhosted.org/packages/75/b2/3573e18eb52ca204ee076a94c145edc80c3df21694648b35ae34c19ac9bb/yarl-1.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f", size = 506742 }, + { url = "https://files.pythonhosted.org/packages/1f/36/f6b5b0fb7c771d5c6c08b7d00a53cd523793454113d4c96460e3f49a1cdd/yarl-1.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc", size = 517070 }, + { url = "https://files.pythonhosted.org/packages/8e/17/48637d4ddcb606f5591afee78d060eab70e172e14766e1fd23453bfed846/yarl-1.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485", size = 502397 }, + { url = "https://files.pythonhosted.org/packages/83/2c/7392645dc1c9eeb8a5485696302a33e3d59bea8a448c8e2f36f98a728e0a/yarl-1.13.1-cp312-cp312-win32.whl", hash = "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320", size = 102343 }, + { url = "https://files.pythonhosted.org/packages/9c/c0/7329799080d7e0bf7b10db417900701ba6810e78a249aef1f4bf3fc2cccb/yarl-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799", size = 111719 }, + { url = "https://files.pythonhosted.org/packages/d3/d2/9542e6207a6e64c32b14b2d9ca4fad6ff80310fc75e70cdbe31680a758c2/yarl-1.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550", size = 186266 }, + { url = "https://files.pythonhosted.org/packages/8b/68/4c6d1aacbc23a05e84c3fab7aaa68c5a7d4531290021c2370fa1e5524fb1/yarl-1.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c", size = 114268 }, + { url = "https://files.pythonhosted.org/packages/ed/87/6ad8e22c918d745092329ec427c0778b5c85ffd5b805e38750024b7464f2/yarl-1.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71", size = 112164 }, + { url = "https://files.pythonhosted.org/packages/ca/5b/c6c4ac4be1edea6759f05ad74d87a1c61329737bdb90da5f66e188310461/yarl-1.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1", size = 471437 }, + { url = "https://files.pythonhosted.org/packages/c1/5c/ec7f0121a5fa67ee76325e1aaa27470d5521d80a25aa1bad5dde773edbe1/yarl-1.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813", size = 485894 }, + { url = "https://files.pythonhosted.org/packages/d7/e8/624fc8082cbff62c537798ce837a6044f70e2e00472ab719deb376ff6e39/yarl-1.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da", size = 486702 }, + { url = "https://files.pythonhosted.org/packages/dc/18/013f7d2e3f0ff28b85299ed19164f899ea4f02da8812621a40937428bf48/yarl-1.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851", size = 478911 }, + { url = "https://files.pythonhosted.org/packages/d7/3c/5b628939e3a22fb9375df453188e97190d21f6244c49637e19799896cd41/yarl-1.13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8", size = 456488 }, + { url = "https://files.pythonhosted.org/packages/8b/2b/a3548db86510c1d95bff344c1c588b84582eeb3a55ea15a149a24d7069f0/yarl-1.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206", size = 475016 }, + { url = "https://files.pythonhosted.org/packages/d8/e2/e2a540f18f849909e3ee594766bf7b0a7fde176ff0cfb2f95121033752e2/yarl-1.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c", size = 477521 }, + { url = "https://files.pythonhosted.org/packages/3a/df/4cda4052da48a57ce4f20a0849b7344902aa3e149a0b409525509fc43985/yarl-1.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c", size = 492000 }, + { url = "https://files.pythonhosted.org/packages/bf/b6/180dbb0aa846cafb9ce89bd33c477e200dd00072c7775372f34651c20b9a/yarl-1.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734", size = 502195 }, + { url = "https://files.pythonhosted.org/packages/ff/37/e97c280344342e326a1860a70054a0488c379e8937325f97f9a9fe6b453d/yarl-1.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26", size = 492892 }, + { url = "https://files.pythonhosted.org/packages/ed/97/cd35f39ba8183ef193a6709aa0b2fcaabebd6915202d6999b01fa630b2bb/yarl-1.13.1-cp313-cp313-win32.whl", hash = "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d", size = 486463 }, + { url = "https://files.pythonhosted.org/packages/05/33/bd9d33503a0f73d095b01ed438423b924e6786e90102ca4912e573cc5aa3/yarl-1.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8", size = 493804 }, + { url = "https://files.pythonhosted.org/packages/74/81/419c24f7c94f56b96d04955482efb5b381635ad265b5b7fbab333a9dfde3/yarl-1.13.1-py3-none-any.whl", hash = "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0", size = 39862 }, +] From 012083e20aba0d09b2adff7d1939fc894518b0b2 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 16:15:35 +0200 Subject: [PATCH 16/51] :wrench: Move mypy config to `pyproject.toml` --- mypy.ini | 7 ------- pyproject.toml | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index ba1fe38..0000000 --- a/mypy.ini +++ /dev/null @@ -1,7 +0,0 @@ -[mypy] -warn_unused_configs = True -disallow_untyped_defs = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_return_any = True -warn_unreachable = True diff --git a/pyproject.toml b/pyproject.toml index e3b40c9..61ccd25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,14 @@ readme = "README.md" requires-python = ">=3.11" version = "0.0.1" +[tool.mypy] +disallow_untyped_defs = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + [tool.pytest.ini_options] filterwarnings = [ "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning" From b42daf92282c1dafec6f9a8aec4627275df36bb1 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 16:34:57 +0200 Subject: [PATCH 17/51] :truck: Move database and state code to `database` module --- eadk_discord/bot.py | 6 +- eadk_discord/bot_setup.py | 2 +- eadk_discord/database/__init__.py | 1 + eadk_discord/{ => database}/database.py | 3 +- eadk_discord/{ => database}/event.py | 0 eadk_discord/database/event_errors.py | 131 ++++++++++++++++++++++ eadk_discord/{ => database}/history.py | 0 eadk_discord/{ => database}/state.py | 143 +++--------------------- tests/test_commands.py | 4 +- 9 files changed, 152 insertions(+), 138 deletions(-) create mode 100644 eadk_discord/database/__init__.py rename eadk_discord/{ => database}/database.py (96%) rename eadk_discord/{ => database}/event.py (100%) create mode 100644 eadk_discord/database/event_errors.py rename eadk_discord/{ => database}/history.py (100%) rename eadk_discord/{ => database}/state.py (70%) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 75b06fd..917c8fc 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -1,17 +1,17 @@ +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime from zoneinfo import ZoneInfo import discord from beartype import beartype -from beartype.typing import Callable from discord.app_commands import AppCommandError from pydantic import BaseModel, Field from eadk_discord import dates, fmt from eadk_discord.database import Database -from eadk_discord.event import BookDesk, Event, MakeFlex, MakeOwned, UnbookDesk -from eadk_discord.state import HandleEventError +from eadk_discord.database.event import BookDesk, Event, MakeFlex, MakeOwned, UnbookDesk +from eadk_discord.database.event_errors import HandleEventError TIME_ZONE = ZoneInfo("Europe/Copenhagen") diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index d90a239..c032f2c 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -12,7 +12,7 @@ from eadk_discord.bot import CommandInfo, EADKBot, Response from eadk_discord.database import Database -from eadk_discord.event import Event, SetNumDesks +from eadk_discord.database.event import Event, SetNumDesks TEST_SERVER_ROLE_ID = ***REMOVED*** EADK_DESK_ADMIN_ID = ***REMOVED*** diff --git a/eadk_discord/database/__init__.py b/eadk_discord/database/__init__.py new file mode 100644 index 0000000..59e3c0b --- /dev/null +++ b/eadk_discord/database/__init__.py @@ -0,0 +1 @@ +from .database import Database as Database diff --git a/eadk_discord/database.py b/eadk_discord/database/database.py similarity index 96% rename from eadk_discord/database.py rename to eadk_discord/database/database.py index d94ee96..8160af2 100644 --- a/eadk_discord/database.py +++ b/eadk_discord/database/database.py @@ -4,9 +4,8 @@ from beartype import beartype from pydantic import BaseModel, Field -from eadk_discord.history import History - from .event import Event +from .history import History from .state import State diff --git a/eadk_discord/event.py b/eadk_discord/database/event.py similarity index 100% rename from eadk_discord/event.py rename to eadk_discord/database/event.py diff --git a/eadk_discord/database/event_errors.py b/eadk_discord/database/event_errors.py new file mode 100644 index 0000000..051d085 --- /dev/null +++ b/eadk_discord/database/event_errors.py @@ -0,0 +1,131 @@ +from collections.abc import Callable +from dataclasses import dataclass +from datetime import date as Date # noqa: N812 + +from eadk_discord.database.event import Event + + +class EventError(Exception): + def message(self, format_user: Callable[[int], str]) -> str: + raise NotImplementedError() + + +@dataclass +class HandleEventError(Exception): + event: Event + error: EventError + + def message(self, format_user: Callable[[int], str]) -> str: + return self.error.message(format_user) + + +@dataclass +class DateTooEarlyError(EventError): + """ + Raised when trying to access a date before the start date. + """ + + date: Date + start_date: Date + + def message(self, format_user: Callable[[int], str]) -> str: + return f"Date {self.date} is before the start date {self.start_date}." + + +@dataclass +class RemoveDeskError(EventError): + """ + Raised when a desk cannot be removed because it is still booked. + """ + + booker: int | None + owner: int | None + desk_index: int + day: Date + + def message(self, format_user: Callable[[int], str]) -> str: + if self.booker and self.owner is None: + return ( + f"Desk {self.desk_index + 1} on {self.day} cannot be removed " + f"because it is booked by {format_user(self.booker)}" + ) + elif (self.booker is None and self.owner) or self.booker and self.booker == self.owner: + return ( + f"Desk {self.desk_index + 1} on {self.day} cannot be removed " + f"because it is owned by {format_user(self.owner)}" + ) + elif self.booker and self.owner: + return ( + f"Desk {self.desk_index + 1} on {self.day} cannot be removed " + f"because it is booked by {format_user(self.booker)} and owned by {format_user(self.owner)}." + ) + else: + raise ValueError("booker and owner cannot both be None") + + +@dataclass +class NonExistentDeskError(EventError): + """ + Raised when trying to book a desk that does not exist. + """ + + desk: int + num_desks: int + day: Date + + def message(self, format_user: Callable[[int], str]) -> str: + return f"Desk {self.desk + 1} on {self.day} does not exist. On that day there are only {self.num_desks} desks." + + +@dataclass +class DeskAlreadyBookedError(EventError): + """ + Raised when trying to book a desk that is already booked. + """ + + booker: int + desk: int + day: Date + + def message(self, format_user: Callable[[int], str]) -> str: + return f"Desk {self.desk + 1} on {self.day} is already booked by {format_user(self.booker)}." + + +@dataclass +class DeskNotBookedError(EventError): + """ + Raised when trying to unbook a desk that is not booked. + """ + + desk: int + day: Date + + def message(self, format_user: Callable[[int], str]) -> str: + return f"Desk {self.desk + 1} on {self.day} is not booked." + + +@dataclass +class DeskAlreadyOwnedError(EventError): + """ + Raised when trying to permanently book a desk that is already permanently booked. + """ + + owner: int + desk: int + day: Date + + def message(self, format_user: Callable[[int], str]) -> str: + return f"Desk {self.desk + 1} on {self.day} is already owned by {format_user(self.owner)}." + + +@dataclass +class DeskNotOwnedError(EventError): + """ + Raised when trying to make an unowned desk flex. + """ + + desk: int + day: Date + + def message(self, format_user: Callable[[int], str]) -> str: + return f"Desk {self.desk + 1} on {self.day} is already a flex desk." diff --git a/eadk_discord/history.py b/eadk_discord/database/history.py similarity index 100% rename from eadk_discord/history.py rename to eadk_discord/database/history.py diff --git a/eadk_discord/state.py b/eadk_discord/database/state.py similarity index 70% rename from eadk_discord/state.py rename to eadk_discord/database/state.py index 2637c25..9fe3493 100644 --- a/eadk_discord/state.py +++ b/eadk_discord/database/state.py @@ -1,17 +1,26 @@ import itertools -from dataclasses import dataclass from datetime import date as Date # noqa: N812 from datetime import timedelta as TimeDelta # noqa: N812 from beartype import beartype -from beartype.typing import Callable, Sequence # noqa: N812 +from beartype.typing import Sequence # noqa: N812 from pydantic import BaseModel, Field +from eadk_discord.database.event_errors import ( + DateTooEarlyError, + DeskAlreadyBookedError, + DeskAlreadyOwnedError, + DeskNotBookedError, + DeskNotOwnedError, + EventError, + HandleEventError, + NonExistentDeskError, + RemoveDeskError, +) + from .event import BookDesk, Event, MakeFlex, MakeOwned, SetNumDesks, UnbookDesk from .history import History -MAX_FUTURE_DAYS: int = 366 - class DeskStatus(BaseModel): booker: int | None = Field(serialization_alias="booker") @@ -120,132 +129,6 @@ def booked_desks(self, member: int) -> list[int]: return [i for i, desk in enumerate(self.desks) if desk.booker == member] -class EventError(Exception): - def message(self, format_user: Callable[[int], str]) -> str: - raise NotImplementedError() - - -@dataclass -class HandleEventError(Exception): - event: Event - error: EventError - - def message(self, format_user: Callable[[int], str]) -> str: - return self.error.message(format_user) - - -@dataclass -class DateTooEarlyError(EventError): - """ - Raised when trying to access a date before the start date. - """ - - date: Date - start_date: Date - - def message(self, format_user: Callable[[int], str]) -> str: - return f"Date {self.date} is before the start date {self.start_date}." - - -@dataclass -class RemoveDeskError(EventError): - """ - Raised when a desk cannot be removed because it is still booked. - """ - - booker: int | None - owner: int | None - desk_index: int - day: Date - - def message(self, format_user: Callable[[int], str]) -> str: - if self.booker and self.owner is None: - return ( - f"Desk {self.desk_index + 1} on {self.day} cannot be removed " - f"because it is booked by {format_user(self.booker)}" - ) - elif (self.booker is None and self.owner) or self.booker and self.booker == self.owner: - return ( - f"Desk {self.desk_index + 1} on {self.day} cannot be removed " - f"because it is owned by {format_user(self.owner)}" - ) - elif self.booker and self.owner: - return ( - f"Desk {self.desk_index + 1} on {self.day} cannot be removed " - f"because it is booked by {format_user(self.booker)} and owned by {format_user(self.owner)}." - ) - else: - raise ValueError("booker and owner cannot both be None") - - -@dataclass -class NonExistentDeskError(EventError): - """ - Raised when trying to book a desk that does not exist. - """ - - desk: int - num_desks: int - day: Date - - def message(self, format_user: Callable[[int], str]) -> str: - return f"Desk {self.desk + 1} on {self.day} does not exist. On that day there are only {self.num_desks} desks." - - -@dataclass -class DeskAlreadyBookedError(EventError): - """ - Raised when trying to book a desk that is already booked. - """ - - booker: int - desk: int - day: Date - - def message(self, format_user: Callable[[int], str]) -> str: - return f"Desk {self.desk + 1} on {self.day} is already booked by {format_user(self.booker)}." - - -@dataclass -class DeskNotBookedError(EventError): - """ - Raised when trying to unbook a desk that is not booked. - """ - - desk: int - day: Date - - def message(self, format_user: Callable[[int], str]) -> str: - return f"Desk {self.desk + 1} on {self.day} is not booked." - - -@dataclass -class DeskAlreadyOwnedError(EventError): - """ - Raised when trying to permanently book a desk that is already permanently booked. - """ - - owner: int - desk: int - day: Date - - def message(self, format_user: Callable[[int], str]) -> str: - return f"Desk {self.desk + 1} on {self.day} is already owned by {format_user(self.owner)}." - - -@dataclass -class DeskNotOwnedError(EventError): - """ - Raised when trying to make an unowned desk flex. - """ - - desk: int - day: Date - - def message(self, format_user: Callable[[int], str]) -> str: - return f"Desk {self.desk + 1} on {self.day} is already a flex desk." - - class State(BaseModel): start_date: Date = Field(serialization_alias="start_date") days: list[Day] = Field(serialization_alias="days") diff --git a/tests/test_commands.py b/tests/test_commands.py index 2b9e19c..1d94df5 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4,8 +4,8 @@ from eadk_discord.bot import CommandInfo, EADKBot from eadk_discord.database import Database -from eadk_discord.event import Event, SetNumDesks -from eadk_discord.state import DateTooEarlyError +from eadk_discord.database.event import Event, SetNumDesks +from eadk_discord.database.state import DateTooEarlyError NOW: datetime = datetime.fromisoformat("2024-09-14") # Saturday TODAY: date = NOW.date() From 064530822c2befcd6b4d03f7bfae0ce49655fbb8 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 17:13:18 +0200 Subject: [PATCH 18/51] :fire: Remove `HandleEventError` --- eadk_discord/bot.py | 4 ++-- eadk_discord/database/event_errors.py | 11 ----------- eadk_discord/database/state.py | 27 +++++++++++---------------- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 917c8fc..8df82e8 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -11,7 +11,7 @@ from eadk_discord import dates, fmt from eadk_discord.database import Database from eadk_discord.database.event import BookDesk, Event, MakeFlex, MakeOwned, UnbookDesk -from eadk_discord.database.event_errors import HandleEventError +from eadk_discord.database.event_errors import EventError TIME_ZONE = ZoneInfo("Europe/Copenhagen") @@ -230,6 +230,6 @@ def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: ) if isinstance(error, discord.app_commands.errors.CommandInvokeError): match error.__cause__: - case HandleEventError(_event, event_error): + case EventError() as event_error: return Response(message=event_error.message(info.format_user), ephemeral=True) raise error diff --git a/eadk_discord/database/event_errors.py b/eadk_discord/database/event_errors.py index 051d085..d17698e 100644 --- a/eadk_discord/database/event_errors.py +++ b/eadk_discord/database/event_errors.py @@ -2,23 +2,12 @@ from dataclasses import dataclass from datetime import date as Date # noqa: N812 -from eadk_discord.database.event import Event - class EventError(Exception): def message(self, format_user: Callable[[int], str]) -> str: raise NotImplementedError() -@dataclass -class HandleEventError(Exception): - event: Event - error: EventError - - def message(self, format_user: Callable[[int], str]) -> str: - return self.error.message(format_user) - - @dataclass class DateTooEarlyError(EventError): """ diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index 9fe3493..38eb59b 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -12,8 +12,6 @@ DeskAlreadyOwnedError, DeskNotBookedError, DeskNotOwnedError, - EventError, - HandleEventError, NonExistentDeskError, RemoveDeskError, ) @@ -155,20 +153,17 @@ def day(self, date: Date) -> tuple[Day, int]: @beartype def handle_event(self, event: Event) -> None: - try: - match event.event: - case SetNumDesks(): - self._set_num_desks(event.event) - case BookDesk(): - self._book_desk(event.event) - case UnbookDesk(): - self._unbook_desk(event.event) - case MakeOwned(): - self._make_owned(event.event) - case MakeFlex(): - self._make_flex(event.event) - except EventError as e: - raise HandleEventError(event=event, error=e) from e + match event.event: + case SetNumDesks(): + self._set_num_desks(event.event) + case BookDesk(): + self._book_desk(event.event) + case UnbookDesk(): + self._unbook_desk(event.event) + case MakeOwned(): + self._make_owned(event.event) + case MakeFlex(): + self._make_flex(event.event) @beartype def _set_num_desks(self, event: SetNumDesks) -> None: From 0009dba63ea3ea9b84d34f193add6e3f3e9f75c5 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 18:46:01 +0200 Subject: [PATCH 19/51] :bug: Add tests and fix bugs --- eadk_discord/bot.py | 2 +- eadk_discord/database/event.py | 10 +- eadk_discord/database/state.py | 4 +- tests/test_book.py | 247 +++++++++++++++++++++++++++++++++ tests/test_commands.py | 42 ------ 5 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 tests/test_book.py delete mode 100644 tests/test_commands.py diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 8df82e8..ddf82f9 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -99,7 +99,7 @@ def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, des ephemeral=True, ) - if desk_arg: + if desk_arg is not None: desk_index = desk_arg - 1 desk_num = desk_arg else: diff --git a/eadk_discord/database/event.py b/eadk_discord/database/event.py index 88ea2e7..ca747a6 100644 --- a/eadk_discord/database/event.py +++ b/eadk_discord/database/event.py @@ -6,29 +6,29 @@ class SetNumDesks(BaseModel): date: Date = Field() - num_desks: int = Field(ge=0) + num_desks: int = Field() class BookDesk(BaseModel): date: Date = Field() - desk_index: int = Field(ge=0) + desk_index: int = Field() user: int = Field() class UnbookDesk(BaseModel): date: Date = Field() - desk_index: int = Field(ge=0) + desk_index: int = Field() class MakeOwned(BaseModel): start_date: Date = Field() - desk_index: int = Field(ge=0) + desk_index: int = Field() user: int = Field() class MakeFlex(BaseModel): start_date: Date = Field() - desk_index: int = Field(ge=0) + desk_index: int = Field() class Event(BaseModel): diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index 38eb59b..f575677 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -188,10 +188,10 @@ def _set_num_desks(self, event: SetNumDesks) -> None: def _book_desk(self, event: BookDesk) -> None: day, _ = self.day(event.date) desk_index = event.desk_index - if desk_index >= len(day.desks): + if desk_index >= len(day.desks) or desk_index < 0: raise NonExistentDeskError(desk=desk_index, num_desks=len(day.desks), day=event.date) desk = day.desks[desk_index] - if desk.booker: + if desk.booker is not None: raise DeskAlreadyBookedError(booker=desk.booker, desk=desk_index, day=event.date) desk.booker = event.user diff --git a/tests/test_book.py b/tests/test_book.py new file mode 100644 index 0000000..b193693 --- /dev/null +++ b/tests/test_book.py @@ -0,0 +1,247 @@ +from datetime import date, datetime, timedelta +from itertools import chain + +import pytest + +from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.database import Database +from eadk_discord.database.event import Event, SetNumDesks +from eadk_discord.database.event_errors import DeskAlreadyBookedError, NonExistentDeskError +from eadk_discord.database.state import DateTooEarlyError + +NOW: datetime = datetime.fromisoformat("2024-09-13") # Friday +TODAY: date = NOW.date() + + +@pytest.fixture +def bot() -> EADKBot: + database = Database.initialize(TODAY) + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=TODAY, num_desks=6))) + + bot = EADKBot(database) + + return bot + + +def test_book(bot: EADKBot) -> None: + database = bot.database + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=None, + user_id=None, + desk_arg=None, + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(0).booker == 1 + for i in range(1, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_with_desk(bot: EADKBot) -> None: + database = bot.database + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, desk_arg=5 + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(4).booker == 1 + for i in range(0, 4): + assert database.state.day(TODAY)[0].desk(i).booker is None + assert database.state.day(TODAY)[0].desk(5).booker is None + + +def test_book2(bot: EADKBot) -> None: + database = bot.database + + database.state.day(TODAY)[0].desk(0).booker = 0 + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=None, + user_id=None, + desk_arg=None, + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(0).booker == 0 + assert database.state.day(TODAY)[0].desk(1).booker == 1 + for i in range(2, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_with_user(bot: EADKBot) -> None: + database = bot.database + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=7, desk_arg=None + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(0).booker == 7 + for i in range(1, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_with_user_desk(bot: EADKBot) -> None: + database = bot.database + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=4, desk_arg=5 + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(4).booker == 4 + for i in range(0, 4): + assert database.state.day(TODAY)[0].desk(i).booker is None + assert database.state.day(TODAY)[0].desk(5).booker is None + + +def test_book_with_date(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(1) + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="tomorrow", + user_id=None, + desk_arg=None, + ) + assert response.ephemeral is False + assert database.state.day(tomorrow)[0].desk(0).booker == 1 + for i in range(1, 6): + assert database.state.day(tomorrow)[0].desk(i).booker is None + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_with_date_desk(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(1) + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="tomorrow", + user_id=None, + desk_arg=3, + ) + assert response.ephemeral is False + assert database.state.day(tomorrow)[0].desk(2).booker == 1 + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(tomorrow)[0].desk(i).booker is None + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_with_date_user(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(1) + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="tomorrow", + user_id=8, + desk_arg=None, + ) + assert response.ephemeral is False + assert database.state.day(tomorrow)[0].desk(0).booker == 8 + for i in range(1, 6): + assert database.state.day(tomorrow)[0].desk(i).booker is None + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_weekday_same_week(bot: EADKBot) -> None: + database = bot.database + + sunday = TODAY + timedelta(2) + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="sunday", + user_id=None, + desk_arg=None, + ) + + assert response.ephemeral is False + assert database.state.day(sunday)[0].desk(0).booker == 1 + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + for i in range(1, 6): + assert database.state.day(sunday)[0].desk(i).booker is None + + +def test_book_weekday_next_week(bot: EADKBot) -> None: + database = bot.database + + tuesday = TODAY + timedelta(4) + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="tuesday", + user_id=None, + desk_arg=None, + ) + + assert response.ephemeral is False + assert database.state.day(tuesday)[0].desk(0).booker == 1 + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + for i in range(1, 6): + assert database.state.day(tuesday)[0].desk(i).booker is None + + +def test_book_date(bot: EADKBot) -> None: + database = bot.database + + date = TODAY + timedelta(23) + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=date.isoformat(), + user_id=None, + desk_arg=None, + ) + assert response.ephemeral is False + assert database.state.day(date)[0].desk(0).booker == 1 + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_book_with_date_user_desk(bot: EADKBot) -> None: + database = bot.database + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="today", user_id=3, desk_arg=2 + ) + assert response.ephemeral is False + assert database.state.day(TODAY)[0].desk(0).booker is None + assert database.state.day(TODAY)[0].desk(1).booker == 3 + + +def test_book_too_early(bot: EADKBot) -> None: + with pytest.raises(DateTooEarlyError): + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + (TODAY - timedelta(1)).isoformat(), + user_id=0, + desk_arg=1, + ) + + +def test_book_non_existant_desk(bot: EADKBot) -> None: + with pytest.raises(NonExistentDeskError): + bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_arg=7) + with pytest.raises(NonExistentDeskError): + bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_arg=0) + + +def test_book_already_booked(bot: EADKBot) -> None: + database = bot.database + + database.state.day(TODAY)[0].desk(0).booker = 0 + + with pytest.raises(DeskAlreadyBookedError): + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=None, desk_arg=1 + ) diff --git a/tests/test_commands.py b/tests/test_commands.py deleted file mode 100644 index 1d94df5..0000000 --- a/tests/test_commands.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import date, datetime, timedelta - -import pytest - -from eadk_discord.bot import CommandInfo, EADKBot -from eadk_discord.database import Database -from eadk_discord.database.event import Event, SetNumDesks -from eadk_discord.database.state import DateTooEarlyError - -NOW: datetime = datetime.fromisoformat("2024-09-14") # Saturday -TODAY: date = NOW.date() - - -@pytest.fixture -def bot() -> EADKBot: - database = Database.initialize(TODAY) - database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=TODAY, num_desks=6))) - - bot = EADKBot(database) - - return bot - - -def test_book(bot: EADKBot) -> None: - database = bot.database - - response = bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=3, desk_arg=2 - ) - assert response.ephemeral is False - assert database.state.day(TODAY)[0].desk(0).booker is None - assert database.state.day(TODAY)[0].desk(1).booker == 3 - - -def test_book_too_early(bot: EADKBot) -> None: - with pytest.raises(DateTooEarlyError): - bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), - (TODAY - timedelta(1)).isoformat(), - user_id=0, - desk_arg=1, - ) From 02b394674ee4045eaf33a4573f0c7b68f196aefe Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 18:56:16 +0200 Subject: [PATCH 20/51] :fire: Remove unused file --- eadk_discord/date_converter.py | 42 ---------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 eadk_discord/date_converter.py diff --git a/eadk_discord/date_converter.py b/eadk_discord/date_converter.py deleted file mode 100644 index 19d70f3..0000000 --- a/eadk_discord/date_converter.py +++ /dev/null @@ -1,42 +0,0 @@ -from dataclasses import dataclass -from datetime import date, timedelta - -from discord import Interaction -from discord.app_commands import Transformer - -WEEKDAYS = [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", -] - - -@dataclass -class DateParseError(Exception): - argument: str - - -def parse_date_arg(argument: str, today: date) -> date: - if argument.lower() == "today": - return today - elif argument.lower() == "tomorrow": - return today + timedelta(days=1) - elif argument.lower() in WEEKDAYS: - weekday = WEEKDAYS.index(argument.lower()) - return today + timedelta(days=(weekday - today.weekday()) % 7) - # Try to parse the argument as an integer representing the day of the month - else: - try: - return date.fromisoformat(argument) - except Exception: - raise DateParseError(argument) from Exception - - -class DateConverter(Transformer): - async def transform(self, interaction: Interaction, argument: str) -> date: - today = date.today() - return parse_date_arg(argument, today) From 6d624cbbce14f24acdac60d22fb4095f7bbf64e1 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 19:01:35 +0200 Subject: [PATCH 21/51] Support test coverage --- eadk_discord/__main__.py | 1 + eadk_discord/bot_setup.py | 1 + eadk_discord/fmt.py | 1 + pyproject.toml | 6 +++ uv.lock | 77 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/eadk_discord/__main__.py b/eadk_discord/__main__.py index 8d4a4a9..485ec26 100644 --- a/eadk_discord/__main__.py +++ b/eadk_discord/__main__.py @@ -1,3 +1,4 @@ +# pragma: coverage exclude file import logging import os from pathlib import Path diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index c032f2c..24705f5 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -1,3 +1,4 @@ +# pragma: coverage exclude file import logging from datetime import date, datetime from pathlib import Path diff --git a/eadk_discord/fmt.py b/eadk_discord/fmt.py index cfb6cf9..b90d91a 100644 --- a/eadk_discord/fmt.py +++ b/eadk_discord/fmt.py @@ -1,3 +1,4 @@ +# pragma: coverage exclude file import logging from datetime import date as Date # noqa: N812 diff --git a/pyproject.toml b/pyproject.toml index 61ccd25..4c97ec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,11 @@ readme = "README.md" requires-python = ">=3.11" version = "0.0.1" +[tool.coverage.report] +exclude_also = [ + "\\A(?s:.*# pragma: coverage exclude file.*)\\Z" +] + [tool.mypy] disallow_untyped_defs = true warn_redundant_casts = true @@ -46,6 +51,7 @@ packages = ["eadk_discord"] dev-dependencies = [ "mypy>=1.11.2", "pre-commit>=3.8.0", + "pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.6.8" ] diff --git a/uv.lock b/uv.lock index ea96bd7..ab415e2 100644 --- a/uv.lock +++ b/uv.lock @@ -132,6 +132,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "discord-py" version = "2.4.0" @@ -168,6 +221,7 @@ dev = [ { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, ] @@ -183,6 +237,7 @@ dev = [ { name = "mypy", specifier = ">=1.11.2" }, { name = "pre-commit", specifier = ">=3.8.0" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "ruff", specifier = ">=0.6.8" }, ] @@ -475,6 +530,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -535,6 +603,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/bd/a8b0c64945a92eaeeb8d0283f27a726a776a1c9d12734d990c5fc7a1278c/ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", size = 8669595 }, ] +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" From 0d7a267695f038c92f5227fd73c006bdee391d4b Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 19:14:56 +0200 Subject: [PATCH 22/51] :technologist: Add `Makefile` --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c158a70 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.PHONY: test launch t + +test: + uv run pytest +t: test + +launch: + . ./.env && uv run eadk_discord From fca0644061cd82f6ab4cec66da87612adac2e009 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 19:33:41 +0200 Subject: [PATCH 23/51] :recycle: Move fixture and consts to `conftest.py` --- tests/conftest.py | 20 ++++++++++++++++++++ tests/test_book.py | 18 ++---------------- 2 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3e9c6ee --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,20 @@ +from datetime import date, datetime + +import pytest + +from eadk_discord.bot import EADKBot +from eadk_discord.database.database import Database +from eadk_discord.database.event import Event, SetNumDesks + +NOW: datetime = datetime.fromisoformat("2024-09-13") # Friday +TODAY: date = NOW.date() + + +@pytest.fixture +def bot() -> EADKBot: + database = Database.initialize(TODAY) + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=TODAY, num_desks=6))) + + bot = EADKBot(database) + + return bot diff --git a/tests/test_book.py b/tests/test_book.py index b193693..d844b13 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -1,27 +1,13 @@ -from datetime import date, datetime, timedelta +from datetime import timedelta from itertools import chain import pytest +from conftest import NOW, TODAY from eadk_discord.bot import CommandInfo, EADKBot -from eadk_discord.database import Database -from eadk_discord.database.event import Event, SetNumDesks from eadk_discord.database.event_errors import DeskAlreadyBookedError, NonExistentDeskError from eadk_discord.database.state import DateTooEarlyError -NOW: datetime = datetime.fromisoformat("2024-09-13") # Friday -TODAY: date = NOW.date() - - -@pytest.fixture -def bot() -> EADKBot: - database = Database.initialize(TODAY) - database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=TODAY, num_desks=6))) - - bot = EADKBot(database) - - return bot - def test_book(bot: EADKBot) -> None: database = bot.database From ed4c5e2b445a812ab841c387cb1dfbb2628b9bbe Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 19:35:56 +0200 Subject: [PATCH 24/51] :truck: Make arg naming more consistent --- eadk_discord/bot.py | 16 +++++++--------- tests/test_book.py | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index ddf82f9..181dee6 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -85,7 +85,7 @@ def info(self, info: CommandInfo, date_str: str | None) -> Response: ) @beartype - def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_arg: int | None) -> Response: + def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_num: int | None) -> Response: if user_id is None: user_id = info.author_id @@ -99,9 +99,8 @@ def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, des ephemeral=True, ) - if desk_arg is not None: - desk_index = desk_arg - 1 - desk_num = desk_arg + if desk_num is not None: + desk_index = desk_num - 1 else: desk_index_option = booking_day.get_available_desk() if desk_index_option is not None: @@ -119,7 +118,7 @@ def book(self, info: CommandInfo, date_str: str | None, user_id: int | None, des return Response(message=f"Desk {desk_num} has been booked for {info.format_user(user_id)} on {date_str}.") @beartype - def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk: int | None) -> Response: + def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, desk_num: int | None) -> Response: booking_date = dates.get_booking_date(date_str, info.now) booking_day, _ = self._database.state.day(booking_date) date_str = fmt.date(booking_date) @@ -130,13 +129,12 @@ def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, d ephemeral=True, ) - if desk is not None: - desk_index = desk - 1 - desk_num = desk + if desk_num is not None: + desk_index = desk_num - 1 if user_id is not None: if user_id != booking_day.desk(desk_index).booker: return Response( - message=f"Desk {desk} is not booked by {info.format_user(user_id)} on {date_str}.", + message=f"Desk {desk_num} is not booked by {info.format_user(user_id)} on {date_str}.", ephemeral=True, ) else: diff --git a/tests/test_book.py b/tests/test_book.py index d844b13..b8400b4 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -16,7 +16,7 @@ def test_book(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False assert database.state.day(TODAY)[0].desk(0).booker == 1 @@ -28,7 +28,7 @@ def test_book_with_desk(bot: EADKBot) -> None: database = bot.database response = bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, desk_arg=5 + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, desk_num=5 ) assert response.ephemeral is False assert database.state.day(TODAY)[0].desk(4).booker == 1 @@ -46,7 +46,7 @@ def test_book2(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False assert database.state.day(TODAY)[0].desk(0).booker == 0 @@ -59,7 +59,7 @@ def test_book_with_user(bot: EADKBot) -> None: database = bot.database response = bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=7, desk_arg=None + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=7, desk_num=None ) assert response.ephemeral is False assert database.state.day(TODAY)[0].desk(0).booker == 7 @@ -71,7 +71,7 @@ def test_book_with_user_desk(bot: EADKBot) -> None: database = bot.database response = bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=4, desk_arg=5 + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=4, desk_num=5 ) assert response.ephemeral is False assert database.state.day(TODAY)[0].desk(4).booker == 4 @@ -89,7 +89,7 @@ def test_book_with_date(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="tomorrow", user_id=None, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False assert database.state.day(tomorrow)[0].desk(0).booker == 1 @@ -108,7 +108,7 @@ def test_book_with_date_desk(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="tomorrow", user_id=None, - desk_arg=3, + desk_num=3, ) assert response.ephemeral is False assert database.state.day(tomorrow)[0].desk(2).booker == 1 @@ -127,7 +127,7 @@ def test_book_with_date_user(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="tomorrow", user_id=8, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False assert database.state.day(tomorrow)[0].desk(0).booker == 8 @@ -146,7 +146,7 @@ def test_book_weekday_same_week(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="sunday", user_id=None, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False @@ -166,7 +166,7 @@ def test_book_weekday_next_week(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="tuesday", user_id=None, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False @@ -186,7 +186,7 @@ def test_book_date(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=date.isoformat(), user_id=None, - desk_arg=None, + desk_num=None, ) assert response.ephemeral is False assert database.state.day(date)[0].desk(0).booker == 1 @@ -198,7 +198,7 @@ def test_book_with_date_user_desk(bot: EADKBot) -> None: database = bot.database response = bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="today", user_id=3, desk_arg=2 + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="today", user_id=3, desk_num=2 ) assert response.ephemeral is False assert database.state.day(TODAY)[0].desk(0).booker is None @@ -211,15 +211,15 @@ def test_book_too_early(bot: EADKBot) -> None: CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), (TODAY - timedelta(1)).isoformat(), user_id=0, - desk_arg=1, + desk_num=1, ) def test_book_non_existant_desk(bot: EADKBot) -> None: with pytest.raises(NonExistentDeskError): - bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_arg=7) + bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_num=7) with pytest.raises(NonExistentDeskError): - bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_arg=0) + bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_num=0) def test_book_already_booked(bot: EADKBot) -> None: @@ -229,5 +229,5 @@ def test_book_already_booked(bot: EADKBot) -> None: with pytest.raises(DeskAlreadyBookedError): bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=None, desk_arg=1 + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=None, desk_num=1 ) From e60d3e6a44eeb1508bd6f45338d9c00a7dfb87fa Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 19:59:35 +0200 Subject: [PATCH 25/51] :bug: Check for desk indices below 0 --- eadk_discord/database/state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index f575677..a0cd1e5 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -199,7 +199,7 @@ def _book_desk(self, event: BookDesk) -> None: def _unbook_desk(self, event: UnbookDesk) -> None: day, _ = self.day(event.date) desk_index = event.desk_index - if desk_index >= len(day.desks): + if desk_index >= len(day.desks) or desk_index < 0: raise NonExistentDeskError(desk=desk_index, num_desks=len(day.desks), day=event.date) desk = day.desks[desk_index] if not desk.booker: @@ -210,7 +210,7 @@ def _unbook_desk(self, event: UnbookDesk) -> None: def _make_owned(self, event: MakeOwned) -> None: day, day_index = self.day(event.start_date) desk_index = event.desk_index - if desk_index >= len(day.desks): + if desk_index >= len(day.desks) or desk_index < 0: raise NonExistentDeskError(desk=desk_index, num_desks=len(day.desks), day=event.start_date) for day in self.days[day_index:]: if desk_index >= len(day.desks): @@ -227,7 +227,7 @@ def _make_owned(self, event: MakeOwned) -> None: def _make_flex(self, event: MakeFlex) -> None: day, day_index = self.day(event.start_date) desk_index = event.desk_index - if desk_index >= len(day.desks): + if desk_index >= len(day.desks) or desk_index < 0: raise NonExistentDeskError(desk=desk_index, num_desks=len(day.desks), day=event.start_date) if day.desks[desk_index].owner is None: raise DeskNotOwnedError(desk=desk_index, day=event.start_date) From 80030090c8eb93e9c3c0497776896e40a8db468e Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 20:03:21 +0200 Subject: [PATCH 26/51] :pencil2: Fix typo in test name --- tests/test_book.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_book.py b/tests/test_book.py index b8400b4..cf0586e 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -215,7 +215,7 @@ def test_book_too_early(bot: EADKBot) -> None: ) -def test_book_non_existant_desk(bot: EADKBot) -> None: +def test_book_non_existent_desk(bot: EADKBot) -> None: with pytest.raises(NonExistentDeskError): bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_num=7) with pytest.raises(NonExistentDeskError): From 229e898c190e16c352026a9bb3c62a4f7a65a2b2 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 20:12:33 +0200 Subject: [PATCH 27/51] :fire: Remove unused clause --- eadk_discord/bot.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index 181dee6..fc87449 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -218,16 +218,10 @@ def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: return Response(message="You do not have permission to run this command.", ephemeral=True) if isinstance(error, discord.app_commands.errors.CheckFailure): return Response(message="This command can only be used in the office channel.", ephemeral=True) - if isinstance(error, discord.app_commands.errors.TransformerError): - match error.__cause__: - case dates.DateParseError(arg): - return Response( - message=f"Date {arg} could not be parsed. " - "Please use the format YYYY-MM-DD, 'today', 'tomorrow', or specify a weekday.", - ephemeral=True, - ) if isinstance(error, discord.app_commands.errors.CommandInvokeError): match error.__cause__: case EventError() as event_error: return Response(message=event_error.message(info.format_user), ephemeral=True) + case _: + raise error raise error From db7f42ea8b79829418be27c61627ba5c2809b715 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 20:15:43 +0200 Subject: [PATCH 28/51] :art: Use match instead of ifs --- eadk_discord/bot.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index fc87449..fa51563 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -212,16 +212,13 @@ def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Res @beartype def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: - if isinstance(error, discord.app_commands.errors.MissingAnyRole) or isinstance( - error, discord.app_commands.errors.MissingRole - ): - return Response(message="You do not have permission to run this command.", ephemeral=True) - if isinstance(error, discord.app_commands.errors.CheckFailure): - return Response(message="This command can only be used in the office channel.", ephemeral=True) - if isinstance(error, discord.app_commands.errors.CommandInvokeError): - match error.__cause__: - case EventError() as event_error: - return Response(message=event_error.message(info.format_user), ephemeral=True) - case _: - raise error + match error: + case discord.app_commands.errors.MissingAnyRole() | discord.app_commands.errors.MissingRole(): + return Response(message="You do not have permission to run this command.", ephemeral=True) + case discord.app_commands.errors.CheckFailure(): + return Response(message="This command can only be used in the office channel.", ephemeral=True) + case discord.app_commands.errors.CommandInvokeError() as error: + match error.__cause__: + case EventError() as event_error: + return Response(message=event_error.message(info.format_user), ephemeral=True) raise error From ab3f0bca6981eb54a0d632a77b2bac5fa4dd791d Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 20:22:55 +0200 Subject: [PATCH 29/51] :bug: Raise `NonExistentDeskError` instead of `ValueError` --- eadk_discord/database/state.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index a0cd1e5..fbc0643 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -95,10 +95,8 @@ def desk(self, desk: int) -> DeskStatus: """ Returns the DeskStatus object for the given desk. """ - if desk < 0: - raise ValueError("desk number must be non-negative") - elif desk >= len(self.desks): - raise ValueError("desk number is out of range. There are only {len(self.desks)} desks.") + if desk < 0 or desk >= len(self.desks): + raise NonExistentDeskError(desk=desk, num_desks=len(self.desks), day=self.date) result: DeskStatus = self.desks[desk] return result From 3fec1d5371b9bef1c12750e9fc58a676f126ffff Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 20:26:26 +0200 Subject: [PATCH 30/51] :art: Name all command arguments --- tests/test_book.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_book.py b/tests/test_book.py index cf0586e..78cee35 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -209,7 +209,7 @@ def test_book_too_early(bot: EADKBot) -> None: with pytest.raises(DateTooEarlyError): bot.book( CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), - (TODAY - timedelta(1)).isoformat(), + date_str=(TODAY - timedelta(1)).isoformat(), user_id=0, desk_num=1, ) @@ -217,9 +217,19 @@ def test_book_too_early(bot: EADKBot) -> None: def test_book_non_existent_desk(bot: EADKBot) -> None: with pytest.raises(NonExistentDeskError): - bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_num=7) + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=0, + desk_num=7, + ) with pytest.raises(NonExistentDeskError): - bot.book(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=0, desk_num=0) + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=0, + desk_num=0, + ) def test_book_already_booked(bot: EADKBot) -> None: @@ -229,5 +239,8 @@ def test_book_already_booked(bot: EADKBot) -> None: with pytest.raises(DeskAlreadyBookedError): bot.book( - CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), "today", user_id=None, desk_num=1 + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=None, + desk_num=1, ) From 339135428bb06e91bb41046341c51b4b7e8f35b7 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 20:48:52 +0200 Subject: [PATCH 31/51] :bug: Prevent check for None errors --- eadk_discord/bot.py | 2 +- eadk_discord/database/state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index fa51563..de72548 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -150,7 +150,7 @@ def unbook(self, info: CommandInfo, date_str: str | None, user_id: int | None, d ) desk_booker = booking_day.desk(desk_index).booker - if desk_booker: + if desk_booker is not None: self._database.handle_event( Event( author=info.author_id, diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index fbc0643..f74f57b 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -200,7 +200,7 @@ def _unbook_desk(self, event: UnbookDesk) -> None: if desk_index >= len(day.desks) or desk_index < 0: raise NonExistentDeskError(desk=desk_index, num_desks=len(day.desks), day=event.date) desk = day.desks[desk_index] - if not desk.booker: + if desk.booker is None: raise DeskNotBookedError(desk=desk_index, day=event.date) desk.booker = None From e962b1fa43fb44a85192e99aaa928e54954e2926 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 21:25:57 +0200 Subject: [PATCH 32/51] :white_check_mark: Test invalid date handling --- tests/test_misc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/test_misc.py diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 0000000..05b262e --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,15 @@ +import pytest +from conftest import NOW + +from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.dates import DateParseError + + +def test_date_invalid(bot: EADKBot) -> None: + with pytest.raises(DateParseError): + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="invalid", + user_id=None, + desk_num=None, + ) From cb1d19f9eaf9bc7047781668435882c1f88a4803 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Thu, 3 Oct 2024 21:27:41 +0200 Subject: [PATCH 33/51] :technologist: Prune coverage report --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4c97ec0..0cd4e8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ version = "0.0.1" exclude_also = [ "\\A(?s:.*# pragma: coverage exclude file.*)\\Z" ] +skip_covered = true [tool.mypy] disallow_untyped_defs = true From 4ecea720a993cfd22b34aab2a0b4bd7a516e7aeb Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:30:05 +0200 Subject: [PATCH 34/51] :recycle: Move internal error message to constant --- eadk_discord/bot_setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eadk_discord/bot_setup.py b/eadk_discord/bot_setup.py index 24705f5..4015379 100644 --- a/eadk_discord/bot_setup.py +++ b/eadk_discord/bot_setup.py @@ -19,6 +19,8 @@ EADK_DESK_ADMIN_ID = ***REMOVED*** EADK_DESK_REGULAR_ID = ***REMOVED*** +INTERNAL_ERROR_MESSAGE = "INTERNAL ERROR HAS OCCURRED BEEP BOOP" + def author_id(interaction: Interaction) -> int: return interaction.user.id @@ -154,7 +156,7 @@ async def on_error(interaction: Interaction, error: AppCommandError) -> None: try: await eadk_bot.handle_error(CommandInfo.from_interaction(interaction), error).send(interaction) except Exception: - await Response(message="INTERNAL ERROR HAS OCCURRED BEEP BOOP", ephemeral=True).send(interaction) + await Response(message=INTERNAL_ERROR_MESSAGE, ephemeral=True).send(interaction) raise return bot From 0d968df43654c882dcbba470f767711befe92414 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:31:11 +0200 Subject: [PATCH 35/51] :sparkles: Do not disallow ownership changes in the past --- eadk_discord/bot.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index de72548..d1df8b5 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -169,12 +169,6 @@ def makeowned(self, info: CommandInfo, start_date_str: str, user_id: int | None, booking_date = dates.get_booking_date(start_date_str, info.now) date_str = fmt.date(booking_date) - if booking_date < info.now.date(): - return Response( - message=f"Date {date_str} not available for booking. Desks cannot be made permanent retroactively.", - ephemeral=True, - ) - desk_index = desk_num - 1 if user_id is None: @@ -193,12 +187,6 @@ def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Res booking_date = dates.get_booking_date(start_date_str, info.now) date_str = fmt.date(booking_date) - if booking_date < info.now.date(): - return Response( - message=f"Date {date_str} not available for booking. You cannot make a desk permanent retroactively.", - ephemeral=True, - ) - desk_index = desk_num - 1 self._database.handle_event( From d10a1cd0045c0ea45ee59bdee49c527a09f743f5 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:33:05 +0200 Subject: [PATCH 36/51] :technologist: Add coverage command to Makefile --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index c158a70..2ac8b5e 100644 --- a/Makefile +++ b/Makefile @@ -4,5 +4,8 @@ test: uv run pytest t: test +cov: + uv run pytest --cov --cov-report=term-missing + launch: . ./.env && uv run eadk_discord From 0c6845833e823fd77832873058f81acce64fdc56 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:39:07 +0200 Subject: [PATCH 37/51] :white_check_mark: Exclude `send` from code coverage --- eadk_discord/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index d1df8b5..cc3c049 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -44,7 +44,7 @@ def __init__(self, message: str = "", ephemeral: bool = False, embed: discord.Em self.embed = embed @beartype - async def send(self, interaction: discord.Interaction) -> None: + async def send(self, interaction: discord.Interaction) -> None: # pragma: no cover if self.embed is None: await interaction.response.send_message(self.message, ephemeral=self.ephemeral) else: From db17a93847133240e5dd97e4b87982f2101e0c2c Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:40:16 +0200 Subject: [PATCH 38/51] :white_check_mark: Add test for `info` command --- tests/test_misc.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_misc.py b/tests/test_misc.py index 05b262e..438f0b8 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2,6 +2,7 @@ from conftest import NOW from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.database.event_errors import DateTooEarlyError from eadk_discord.dates import DateParseError @@ -13,3 +14,13 @@ def test_date_invalid(bot: EADKBot) -> None: user_id=None, desk_num=None, ) + + +def test_info(bot: EADKBot) -> None: + response = bot.info(CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None) + assert response.ephemeral is True + + with pytest.raises(DateTooEarlyError): + response = bot.info( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="2021-01-01" + ) From 3fdec3169999e0246c3765855b74f8d886219be7 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:43:24 +0200 Subject: [PATCH 39/51] :white_check_mark: Full coverage for `book` command --- tests/test_book.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_book.py b/tests/test_book.py index 78cee35..1c35168 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -5,6 +5,7 @@ from conftest import NOW, TODAY from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.bot_setup import INTERNAL_ERROR_MESSAGE from eadk_discord.database.event_errors import DeskAlreadyBookedError, NonExistentDeskError from eadk_discord.database.state import DateTooEarlyError @@ -205,6 +206,33 @@ def test_book_with_date_user_desk(bot: EADKBot) -> None: assert database.state.day(TODAY)[0].desk(1).booker == 3 +def test_book_in_past(bot: EADKBot) -> None: + response = bot.book( + CommandInfo(now=NOW + timedelta(2), format_user=lambda user: str(user), author_id=1), + date_str=TODAY.isoformat(), + user_id=None, + desk_num=None, + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE + + +def test_book_fully_booked(bot: EADKBot) -> None: + database = bot.database + + for i in range(6): + database.state.day(TODAY)[0].desk(i).booker = i + + response = bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=7), + date_str=None, + user_id=None, + desk_num=None, + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE + + def test_book_too_early(bot: EADKBot) -> None: with pytest.raises(DateTooEarlyError): bot.book( From f81113ef2d520b7780b6ccd720add627d5691916 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:49:11 +0200 Subject: [PATCH 40/51] :white_check_mark: Full coverage for `unbook` command --- tests/test_unbook.py | 168 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/test_unbook.py diff --git a/tests/test_unbook.py b/tests/test_unbook.py new file mode 100644 index 0000000..2bd160c --- /dev/null +++ b/tests/test_unbook.py @@ -0,0 +1,168 @@ +from datetime import timedelta + +import pytest +from conftest import NOW, TODAY + +from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.bot_setup import INTERNAL_ERROR_MESSAGE +from eadk_discord.database.event_errors import NonExistentDeskError +from eadk_discord.database.state import DateTooEarlyError + + +def test_unbook(bot: EADKBot) -> None: + database = bot.database + + database.state.day(TODAY)[0].desk(3).booker = 1 + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=None, + user_id=None, + desk_num=None, + ) + assert response.ephemeral is False + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + + +def test_unbook_with_desk(bot: EADKBot) -> None: + state = bot.database.state + + state.day(TODAY)[0].desk(4).booker = 1 + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, desk_num=5 + ) + assert response.ephemeral is False + for i in range(0, 6): + assert state.day(TODAY)[0].desk(i).booker is None + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=None, desk_num=3 + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE + for i in range(0, 6): + assert state.day(TODAY)[0].desk(i).booker is None + + +def test_unbook_with_user(bot: EADKBot) -> None: + state = bot.database.state + + day = state.day(TODAY)[0] + day.desk(3).booker = 5 + day.desk(1).booker = 4 + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=5, desk_num=None + ) + assert response.ephemeral is False + assert day.desk(0).booker is None + assert day.desk(1).booker == 4 + for i in range(2, 6): + assert day.desk(i).booker is None + + +def test_unbook_with_user_desk(bot: EADKBot) -> None: + state = bot.database.state + + day = state.day(TODAY)[0] + day.desk(3).booker = 5 + day.desk(1).booker = 4 + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=5, desk_num=4 + ) + assert response.ephemeral is False + assert day.desk(1).booker == 4 + assert day.desk(3).booker is None + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str=None, user_id=5, desk_num=2 + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE + + +def test_unbook_with_date(bot: EADKBot) -> None: + state = bot.database.state + + date = TODAY + timedelta(3) + + today = state.day(TODAY)[0] + day = state.day(date)[0] + today.desk(3).booker = 1 + today.desk(4).booker = 4 + day.desk(0).booker = 3 + day.desk(4).booker = 1 + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=date.isoformat(), + user_id=None, + desk_num=None, + ) + assert response.ephemeral is False + assert today.desk(3).booker == 1 + assert today.desk(4).booker == 4 + assert day.desk(0).booker == 3 + for i in range(1, 6): + assert day.desk(i).booker is None + + +def test_unbook_in_past(bot: EADKBot) -> None: + response = bot.unbook( + CommandInfo(now=NOW + timedelta(2), format_user=lambda user: str(user), author_id=1), + date_str=TODAY.isoformat(), + user_id=None, + desk_num=None, + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE + + +def test_unbook_too_early(bot: EADKBot) -> None: + with pytest.raises(DateTooEarlyError): + bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=(TODAY - timedelta(1)).isoformat(), + user_id=0, + desk_num=1, + ) + + +def test_unbook_non_existent_desk(bot: EADKBot) -> None: + with pytest.raises(NonExistentDeskError): + bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=0, + desk_num=7, + ) + with pytest.raises(NonExistentDeskError): + bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=0, + desk_num=0, + ) + + +def test_unbook_unbooked_desk(bot: EADKBot) -> None: + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=0, + desk_num=None, + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE + + response = bot.unbook( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str="today", + user_id=0, + desk_num=1, + ) + assert response.ephemeral is True + assert response.message != INTERNAL_ERROR_MESSAGE From 108965d921c6654f3fb70e2c7e92d4e3bf9a14aa Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 11:59:58 +0200 Subject: [PATCH 41/51] :white_check_mark: Full coverage for `makeowned` command --- tests/test_makeowned.py | 135 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/test_makeowned.py diff --git a/tests/test_makeowned.py b/tests/test_makeowned.py new file mode 100644 index 0000000..6c29e5a --- /dev/null +++ b/tests/test_makeowned.py @@ -0,0 +1,135 @@ +from datetime import timedelta +from itertools import chain + +import pytest +from conftest import NOW, TODAY + +from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.database.event_errors import DeskAlreadyOwnedError, NonExistentDeskError + + +def test_makeowned(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + distant_date = TODAY + timedelta(days=23) + + response = bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=3, + ) + assert response.ephemeral is False + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + assert database.state.day(TODAY)[0].desk(i).owner is None + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(tomorrow)[0].desk(i).booker is None + assert database.state.day(tomorrow)[0].desk(i).owner is None + assert database.state.day(tomorrow)[0].desk(2).booker == 1 + assert database.state.day(tomorrow)[0].desk(2).owner == 1 + + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(distant_date)[0].desk(i).booker is None + assert database.state.day(distant_date)[0].desk(i).owner is None + assert database.state.day(distant_date)[0].desk(2).booker == 1 + assert database.state.day(distant_date)[0].desk(2).owner == 1 + + +def test_makeowned_with_user(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + distant_date = TODAY + timedelta(days=23) + + response = bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=4, + desk_num=3, + ) + assert response.ephemeral is False + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + assert database.state.day(TODAY)[0].desk(i).owner is None + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(tomorrow)[0].desk(i).booker is None + assert database.state.day(tomorrow)[0].desk(i).owner is None + assert database.state.day(tomorrow)[0].desk(2).booker == 4 + assert database.state.day(tomorrow)[0].desk(2).owner == 4 + + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(distant_date)[0].desk(i).booker is None + assert database.state.day(distant_date)[0].desk(i).owner is None + assert database.state.day(distant_date)[0].desk(2).booker == 4 + assert database.state.day(distant_date)[0].desk(2).owner == 4 + + +def test_makeowned_booked(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + distant_date = TODAY + timedelta(days=23) + + database.state.day(distant_date)[0].desk(2).booker = 4 + + response = bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=3, + ) + assert response.ephemeral is False + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + assert database.state.day(TODAY)[0].desk(i).owner is None + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(tomorrow)[0].desk(i).booker is None + assert database.state.day(tomorrow)[0].desk(i).owner is None + assert database.state.day(tomorrow)[0].desk(2).booker == 1 + assert database.state.day(tomorrow)[0].desk(2).owner == 1 + + for i in chain(range(0, 2), range(3, 6)): + assert database.state.day(distant_date)[0].desk(i).booker is None + assert database.state.day(distant_date)[0].desk(i).owner is None + assert database.state.day(distant_date)[0].desk(2).booker == 4 + assert database.state.day(distant_date)[0].desk(2).owner == 1 + + +def test_makeowned_nonexistent_desk(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + + with pytest.raises(NonExistentDeskError): + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=7, + ) + + for i in range(0, 6): + assert database.state.day(TODAY)[0].desk(i).booker is None + assert database.state.day(TODAY)[0].desk(i).owner is None + for i in range(0, 6): + assert database.state.day(tomorrow)[0].desk(i).booker is None + assert database.state.day(tomorrow)[0].desk(i).owner is None + + +def test_makeowned_owned_desk(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + distant_date = TODAY + timedelta(days=23) + + database.state.day(distant_date)[0].desk(2).owner = 4 + + with pytest.raises(DeskAlreadyOwnedError): + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=3, + ) From 56950ee28d87ef011696febac3a0a9e43f808718 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 12:08:24 +0200 Subject: [PATCH 42/51] :white_check_mark: Exclude save/load methods from coverage --- eadk_discord/database/database.py | 4 ++-- eadk_discord/database/history.py | 8 ++++---- eadk_discord/database/state.py | 22 ---------------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/eadk_discord/database/database.py b/eadk_discord/database/database.py index 8160af2..bf87f43 100644 --- a/eadk_discord/database/database.py +++ b/eadk_discord/database/database.py @@ -21,13 +21,13 @@ def initialize(start_date: Date) -> "Database": return Database(history=history, state=state) @beartype - def save(self, path: Path) -> None: + def save(self, path: Path) -> None: # pragma: no cover with path.open("w") as file: file.write(self.history.to_json()) @beartype @staticmethod - def load(path: Path) -> "Database": + def load(path: Path) -> "Database": # pragma: no cover with path.open("r") as file: data = file.read() history = History.from_json(data) diff --git a/eadk_discord/database/history.py b/eadk_discord/database/history.py index 5f4fbc2..702227c 100644 --- a/eadk_discord/database/history.py +++ b/eadk_discord/database/history.py @@ -15,20 +15,20 @@ class History(BaseModel): def initialize(start_date: Date) -> "History": return History(start_date=start_date, history=[]) - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> dict[str, Any]: # pragma: no cover return self.model_dump() - def to_json(self) -> str: + def to_json(self) -> str: # pragma: no cover return self.model_dump_json() @beartype @staticmethod - def from_json(data: str) -> "History": + def from_json(data: str) -> "History": # pragma: no cover return History.model_validate_json(data) @beartype @staticmethod - def from_dict(data: dict[Any, Any]) -> "History": + def from_dict(data: dict[Any, Any]) -> "History": # pragma: no cover return History.model_validate(data) @beartype diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index f74f57b..7f72f85 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -24,28 +24,6 @@ class DeskStatus(BaseModel): booker: int | None = Field(serialization_alias="booker") owner: int | None = Field(serialization_alias="owner") - @beartype - def _book(self, user: int) -> bool: - """ - Returns True if the desk was successfully booked, False if the desk was already booked. - """ - if self.booker: - return False - else: - self.booker = user - return True - - @beartype - def _unbook(self) -> bool: - """ - Returns True if the desk was successfully unbooked, False if the desk was not booked. - """ - if self.booker: - self.booker = None - return True - else: - return False - @beartype def _make_owned(self, user: int) -> bool: """ From b726b52a06d9c781c04dca6cf05bad0bc9162e99 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 12:12:37 +0200 Subject: [PATCH 43/51] :fire: Remove unneeded methods and logic --- eadk_discord/database/state.py | 35 ++++++++++------------------------ 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/eadk_discord/database/state.py b/eadk_discord/database/state.py index 7f72f85..1cdf4e3 100644 --- a/eadk_discord/database/state.py +++ b/eadk_discord/database/state.py @@ -25,30 +25,22 @@ class DeskStatus(BaseModel): owner: int | None = Field(serialization_alias="owner") @beartype - def _make_owned(self, user: int) -> bool: + def _make_owned(self, user: int) -> None: """ - Returns True if the desk was successfully permanently booked, False if the desk was already permanently booked. + Make the desk owned by the given user. """ - if self.owner: - return False - else: - if self.booker is None: - self.booker = user - self.owner = user - return True + if self.booker is None: + self.booker = user + self.owner = user @beartype - def _make_flex(self) -> bool: + def _make_flex(self) -> None: """ - Returns True if the desk was successfully unpermanently booked, False if the desk was not permanently booked. + Removes ownership of the desk, turning it into a flex desk. """ - if self.owner: - if self.owner == self.booker: - self.booker = None - self.owner = None - return True - else: - return False + if self.owner == self.booker: + self.booker = None + self.owner = None class Day(BaseModel): @@ -88,13 +80,6 @@ def get_available_desk(self) -> int | None: return i return None - @beartype - def available_desks(self) -> list[int]: - """ - Returns a list of indices of available desks. - """ - return [i for i, desk in enumerate(self.desks) if desk.booker is None] - @beartype def booked_desks(self, member: int) -> list[int]: """ From 2aee36a455a7c08665ffc0c996860b4ce1acbc95 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 12:13:34 +0200 Subject: [PATCH 44/51] :white_check_mark: Exclude discord py error handling from coverage --- eadk_discord/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eadk_discord/bot.py b/eadk_discord/bot.py index cc3c049..44f29b2 100644 --- a/eadk_discord/bot.py +++ b/eadk_discord/bot.py @@ -199,7 +199,7 @@ def makeflex(self, info: CommandInfo, start_date_str: str, desk_num: int) -> Res return Response(message=f"Desk {desk_num} is now a flex desk from {date_str} onwards.") @beartype - def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: + def handle_error(self, info: CommandInfo, error: AppCommandError) -> Response: # pragma: no cover match error: case discord.app_commands.errors.MissingAnyRole() | discord.app_commands.errors.MissingRole(): return Response(message="You do not have permission to run this command.", ephemeral=True) From 85d2d09971cf13853b8be09139f06a1214c9b8a9 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 12:19:36 +0200 Subject: [PATCH 45/51] :pencil2: Make test name consistent --- tests/test_makeowned.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_makeowned.py b/tests/test_makeowned.py index 6c29e5a..a7ca484 100644 --- a/tests/test_makeowned.py +++ b/tests/test_makeowned.py @@ -97,7 +97,7 @@ def test_makeowned_booked(bot: EADKBot) -> None: assert database.state.day(distant_date)[0].desk(2).owner == 1 -def test_makeowned_nonexistent_desk(bot: EADKBot) -> None: +def test_makeowned_non_existent_desk(bot: EADKBot) -> None: database = bot.database tomorrow = TODAY + timedelta(days=1) From 66518f296a22792e2a2fcf6c3811a86dc9285055 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 12:38:41 +0200 Subject: [PATCH 46/51] :white_check_mark: Full coverage for command `makeflex` --- tests/test_makeflex.py | 135 ++++++++++++++++++++++++++++++++++++++++ tests/test_makeowned.py | 29 +++++++++ 2 files changed, 164 insertions(+) create mode 100644 tests/test_makeflex.py diff --git a/tests/test_makeflex.py b/tests/test_makeflex.py new file mode 100644 index 0000000..66188d0 --- /dev/null +++ b/tests/test_makeflex.py @@ -0,0 +1,135 @@ +from datetime import timedelta + +import pytest +from conftest import NOW, TODAY + +from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.database.event import Event, SetNumDesks +from eadk_discord.database.event_errors import DeskNotOwnedError, NonExistentDeskError + + +def test_makeflex(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + distant_date = TODAY + timedelta(days=23) + distant_date2 = TODAY + timedelta(days=43) + + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=3, + ) + + assert database.state.day(distant_date)[0].desk(2).booker == 1 + assert database.state.day(distant_date)[0].desk(2).owner == 1 + assert database.state.day(distant_date2)[0].desk(2).booker == 1 + assert database.state.day(distant_date2)[0].desk(2).owner == 1 + + response = bot.makeflex( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=distant_date.isoformat(), + desk_num=3, + ) + assert response.ephemeral is False + assert database.state.day(distant_date - timedelta(days=1))[0].desk(2).booker == 1 + assert database.state.day(distant_date - timedelta(days=1))[0].desk(2).owner == 1 + assert database.state.day(distant_date)[0].desk(2).booker is None + assert database.state.day(distant_date)[0].desk(2).owner is None + assert database.state.day(distant_date2)[0].desk(2).booker is None + assert database.state.day(distant_date2)[0].desk(2).owner is None + + +def test_makeflex_booked(bot: EADKBot) -> None: + database = bot.database + + tomorrow = TODAY + timedelta(days=1) + distant_date = TODAY + timedelta(days=23) + distant_date2 = TODAY + timedelta(days=43) + + database.state.day(distant_date2)[0].desk(2).booker = 4 + + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=3, + ) + + assert database.state.day(distant_date)[0].desk(2).booker == 1 + assert database.state.day(distant_date)[0].desk(2).owner == 1 + assert database.state.day(distant_date2)[0].desk(2).booker == 4 + assert database.state.day(distant_date2)[0].desk(2).owner == 1 + + response = bot.makeflex( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=distant_date.isoformat(), + desk_num=3, + ) + assert response.ephemeral is False + assert database.state.day(distant_date - timedelta(days=1))[0].desk(2).booker == 1 + assert database.state.day(distant_date - timedelta(days=1))[0].desk(2).owner == 1 + assert database.state.day(distant_date)[0].desk(2).booker is None + assert database.state.day(distant_date)[0].desk(2).owner is None + assert database.state.day(distant_date2)[0].desk(2).booker == 4 + assert database.state.day(distant_date2)[0].desk(2).owner is None + + +def test_makeflex_varying_desk_num(bot: EADKBot) -> None: + database = bot.database + + date1 = TODAY + timedelta(days=3) + date2 = TODAY + timedelta(days=7) + + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date1, num_desks=4))) + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date2, num_desks=6))) + + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=TODAY.isoformat(), + user_id=None, + desk_num=5, + ) + + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=date2.isoformat(), + user_id=None, + desk_num=5, + ) + + bot.makeflex( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=(TODAY + timedelta(1)).isoformat(), + desk_num=5, + ) + + assert database.state.day(TODAY)[0].desk(4).owner == 1 + assert database.state.day(TODAY + timedelta(1))[0].desk(4).owner is None + assert database.state.day(date1 - timedelta(days=1))[0].desk(4).owner is None + assert database.state.day(date2)[0].desk(4).owner == 1 + + +def test_makeflex_non_existent_desk(bot: EADKBot) -> None: + with pytest.raises(NonExistentDeskError): + bot.makeflex( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=TODAY.isoformat(), + desk_num=7, + ) + with pytest.raises(NonExistentDeskError): + bot.makeflex( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=TODAY.isoformat(), + desk_num=0, + ) + + +def test_makeflex_flex_desk(bot: EADKBot) -> None: + with pytest.raises(DeskNotOwnedError): + bot.makeflex( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=TODAY.isoformat(), + desk_num=1, + ) diff --git a/tests/test_makeowned.py b/tests/test_makeowned.py index a7ca484..57709b2 100644 --- a/tests/test_makeowned.py +++ b/tests/test_makeowned.py @@ -5,6 +5,7 @@ from conftest import NOW, TODAY from eadk_discord.bot import CommandInfo, EADKBot +from eadk_discord.database.event import Event, SetNumDesks from eadk_discord.database.event_errors import DeskAlreadyOwnedError, NonExistentDeskError @@ -97,6 +98,26 @@ def test_makeowned_booked(bot: EADKBot) -> None: assert database.state.day(distant_date)[0].desk(2).owner == 1 +def test_makeowned_varying_desk_num(bot: EADKBot) -> None: + database = bot.database + + date1 = TODAY + timedelta(days=3) + date2 = TODAY + timedelta(days=7) + + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date1, num_desks=4))) + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date2, num_desks=6))) + + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=TODAY.isoformat(), + user_id=None, + desk_num=5, + ) + + assert database.state.day(date1 - timedelta(days=1))[0].desk(4).owner == 1 + assert database.state.day(date2)[0].desk(4).owner is None + + def test_makeowned_non_existent_desk(bot: EADKBot) -> None: database = bot.database @@ -110,6 +131,14 @@ def test_makeowned_non_existent_desk(bot: EADKBot) -> None: desk_num=7, ) + with pytest.raises(NonExistentDeskError): + bot.makeowned( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + start_date_str=tomorrow.isoformat(), + user_id=None, + desk_num=0, + ) + for i in range(0, 6): assert database.state.day(TODAY)[0].desk(i).booker is None assert database.state.day(TODAY)[0].desk(i).owner is None From d0c0de1b30504dc2a41a98c5f1a40e9d925b85a8 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 12:56:58 +0200 Subject: [PATCH 47/51] :white_check_mark: Remove error message method from coverage --- eadk_discord/database/event_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eadk_discord/database/event_errors.py b/eadk_discord/database/event_errors.py index d17698e..75c89a6 100644 --- a/eadk_discord/database/event_errors.py +++ b/eadk_discord/database/event_errors.py @@ -32,7 +32,7 @@ class RemoveDeskError(EventError): desk_index: int day: Date - def message(self, format_user: Callable[[int], str]) -> str: + def message(self, format_user: Callable[[int], str]) -> str: # pragma: no cover if self.booker and self.owner is None: return ( f"Desk {self.desk_index + 1} on {self.day} cannot be removed " From 6127cf30091a5023f7c8c5c02c9e434f0198042f Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 13:03:04 +0200 Subject: [PATCH 48/51] :white_check_mark: Tests for `SetNumDesks` event --- tests/test_misc.py | 59 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index 438f0b8..49f2dd6 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,8 +1,11 @@ +from datetime import timedelta + import pytest -from conftest import NOW +from conftest import NOW, TODAY from eadk_discord.bot import CommandInfo, EADKBot -from eadk_discord.database.event_errors import DateTooEarlyError +from eadk_discord.database.event import Event, SetNumDesks +from eadk_discord.database.event_errors import DateTooEarlyError, NonExistentDeskError, RemoveDeskError from eadk_discord.dates import DateParseError @@ -24,3 +27,55 @@ def test_info(bot: EADKBot) -> None: response = bot.info( CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), date_str="2021-01-01" ) + + +def test_change_desk_num(bot: EADKBot) -> None: + database = bot.database + + date1 = TODAY + timedelta(days=7) + date2 = TODAY + timedelta(days=42) + + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date1, num_desks=7))) + + database.state.day(date1)[0].desk(6) + database.state.day(date2)[0].desk(6) + with pytest.raises(NonExistentDeskError): + database.state.day(date1)[0].desk(7) + + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date2, num_desks=3))) + + database.state.day(date1)[0].desk(6) + with pytest.raises(NonExistentDeskError): + database.state.day(date2)[0].desk(6) + with pytest.raises(NonExistentDeskError): + database.state.day(date2)[0].desk(3) + with pytest.raises(NonExistentDeskError): + database.state.day(date2 + timedelta(1))[0].desk(3) + database.state.day(date2)[0].desk(2) + + +def test_change_desk_num_owned_or_used(bot: EADKBot) -> None: + database = bot.database + + date1 = TODAY + timedelta(days=7) + date2 = TODAY + timedelta(days=42) + + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=date1.isoformat(), + user_id=None, + desk_num=6, + ) + + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date1, num_desks=7))) + + bot.book( + CommandInfo(now=NOW, format_user=lambda user: str(user), author_id=1), + date_str=date2.isoformat(), + user_id=None, + desk_num=7, + ) + + with pytest.raises(RemoveDeskError) as e: + database.handle_event(Event(author=None, time=NOW, event=SetNumDesks(date=date1, num_desks=6))) + assert e.value.desk_index == 6 From 751cc27e35476a479ddd744461595917e23094ec Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 18:10:11 +0200 Subject: [PATCH 49/51] :construction_worker: Add `pytest` to CI --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cb025c..bcd8f2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,13 @@ jobs: mypy_version: 1.11.2 requirement_files: requirements.txt requirements_dev.txt python_version: '3.12' + + - name: Run pytest + uses: pavelzw/pytest-action@v2 + with: + verbose: true + emoji: true + job-summary: true + custom-arguments: -q + click-to-expand: true + report-title: Test Report From dcb653bdde0ecf7ae0f4a79fcc04b82d0a5ec9c7 Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 18:31:07 +0200 Subject: [PATCH 50/51] :construction_worker: Use `uv` in CI --- .github/workflows/ci.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcd8f2c..39beed2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,24 +25,24 @@ jobs: - name: Validate YAML files run: yamllint . --strict - - name: Ruff - uses: chartboost/ruff-action@v1 + - name: Install uv + uses: astral-sh/setup-uv@v3 with: - src: eadk_discord + version: 0.4.18 - - name: Mypy - uses: jashparekh/mypy-action@v2 + - name: Set up Python + uses: actions/setup-python@v5 with: - mypy_version: 1.11.2 - requirement_files: requirements.txt requirements_dev.txt - python_version: '3.12' + python-version-file: pyproject.toml + + - name: Install the project + run: uv sync --all-extras --dev + + - name: Ruff + run: uv run ruff check + + - name: Mypy + run: uv run mypy . - name: Run pytest - uses: pavelzw/pytest-action@v2 - with: - verbose: true - emoji: true - job-summary: true - custom-arguments: -q - click-to-expand: true - report-title: Test Report + run: uv run pytest --cov --cov-report=term-missing From de7613faff539c8ec4dcf36b915573dc4d97e91a Mon Sep 17 00:00:00 2001 From: Albert Garde Date: Sat, 5 Oct 2024 18:32:49 +0200 Subject: [PATCH 51/51] :fire: Remove no longer needed requirements files --- requirements.txt | 3 --- requirements_dev.txt | 4 ---- 2 files changed, 7 deletions(-) delete mode 100644 requirements.txt delete mode 100644 requirements_dev.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8d6ba5a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -discord.py ~= 2.4.0 -pydantic ~= 2.9.1 -beartype ~= 0.19.0 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 513488f..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -ruff ~= 0.6.5 -mypy ~= 1.11.2 -pre-commit ~= 3.8.0 -pytest ~= 8.3.3