diff --git a/.forgejo/workflows/workflow.yaml b/.forgejo/workflows/workflow.yaml index 98b12fc..f88c687 100644 --- a/.forgejo/workflows/workflow.yaml +++ b/.forgejo/workflows/workflow.yaml @@ -58,7 +58,7 @@ jobs: npx -p "@getmeli/cli" meli upload ./site \ --url "https://pages.coastalcommits.com" \ --site "${{ vars.MELI_SITE_ID }}" \ - --token "${{ secrets.MELI_SECRET }}" \ + --token "${{ secrets.MELI_SITE_SECRET }}" \ --release "$CI_ACTION_REF_NAME_SLUG/${{ env.GITHUB_SHA }}" \ --branch "$CI_ACTION_REF_NAME_SLUG" diff --git a/.gitignore b/.gitignore index 429d31f..3f0e8b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vscode site .venv +__pycache__ diff --git a/aurora/aurora.py b/aurora/aurora.py index 563a82c..1efb03c 100644 --- a/aurora/aurora.py +++ b/aurora/aurora.py @@ -21,17 +21,20 @@ from redbot.core.bot import Red from redbot.core.commands.converter import parse_relativedelta, parse_timedelta from redbot.core.utils.chat_formatting import box, error, humanize_list, humanize_timedelta, warning -from aurora.importers.aurora import ImportAuroraView -from aurora.importers.galacticbot import ImportGalacticBotView -from aurora.menus.addrole import Addrole -from aurora.menus.guild import Guild -from aurora.menus.immune import Immune -from aurora.menus.overrides import Overrides -from aurora.utilities.config import config, register_config -from aurora.utilities.database import connect, create_guild_table, fetch_case, mysql_log -from aurora.utilities.factory import addrole_embed, case_factory, changes_factory, evidenceformat_factory, guild_embed, immune_embed, message_factory, overrides_embed -from aurora.utilities.logger import logger -from aurora.utilities.utils import check_moddable, check_permissions, convert_timedelta_to_str, fetch_channel_dict, fetch_user_dict, generate_dict, get_footer_image, log, send_evidenceformat, timedelta_from_relativedelta +from .importers.aurora import ImportAuroraView +from .importers.galacticbot import ImportGalacticBotView +from .menus.addrole import Addrole +from .menus.guild import Guild +from .menus.immune import Immune +from .menus.overrides import Overrides +from .models.change import Change +from .models.moderation import Moderation +from .utilities.config import config, register_config +from .utilities.database import connect, create_guild_table +from .utilities.factory import addrole_embed, case_factory, changes_factory, evidenceformat_factory, guild_embed, immune_embed, message_factory, overrides_embed +from .utilities.json import dump +from .utilities.logger import logger +from .utilities.utils import check_moddable, check_permissions, get_footer_image, log, send_evidenceformat, timedelta_from_relativedelta class Aurora(commands.Cog): @@ -40,7 +43,7 @@ class Aurora(commands.Cog): This cog stores all of its data in an SQLite database.""" __author__ = ["SeaswimmerTheFsh"] - __version__ = "2.1.3" + __version__ = "2.2.0" __documentation__ = "https://seacogs.coastalcommits.com/aurora/" async def red_delete_data_for_user(self, *, requester, user_id: int): @@ -139,7 +142,7 @@ class Aurora(commands.Cog): if entry.user.id == self.bot.user.id: return - duration = "NULL" + duration = None if entry.reason: reason = entry.reason + " (This action was performed without the bot.)" @@ -172,13 +175,14 @@ class Aurora(commands.Cog): else: return - await mysql_log( + Moderation.log( + self.bot, entry.guild.id, entry.user.id, moderation_type, "USER", entry.target.id, - 0, + None, duration, reason, ) @@ -193,7 +197,7 @@ class Aurora(commands.Cog): interaction: discord.Interaction, target: discord.User, reason: str, - silent: bool = None, + silent: bool | None = None, ): """Add a note to a user. @@ -217,7 +221,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -228,23 +233,22 @@ class Aurora(commands.Cog): except discord.errors.HTTPException: pass - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "NOTE", "USER", target.id, - 0, - "NULL", + None, + None, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has received a note! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has received a note! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="warn") async def warn( @@ -252,7 +256,7 @@ class Aurora(commands.Cog): interaction: discord.Interaction, target: discord.Member, reason: str, - silent: bool = None, + silent: bool | None = None, ): """Warn a user. @@ -276,7 +280,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -287,23 +292,22 @@ class Aurora(commands.Cog): except discord.errors.HTTPException: pass - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "WARN", "USER", target.id, - 0, - "NULL", + None, + None, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been warned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been warned! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="addrole") async def addrole( @@ -312,8 +316,8 @@ class Aurora(commands.Cog): target: discord.Member, role: discord.Role, reason: str, - duration: str = None, - silent: bool = None, + duration: str | None = None, + silent: bool | None = None, ): """Add a role to a user. @@ -346,7 +350,7 @@ class Aurora(commands.Cog): ) return else: - parsed_time = "NULL" + parsed_time = None if role.id not in addrole_whitelist: await interaction.response.send_message( @@ -372,7 +376,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -393,7 +398,8 @@ class Aurora(commands.Cog): content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`" ) - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "ADDROLE", @@ -404,12 +410,10 @@ class Aurora(commands.Cog): reason, ) await response.edit( - content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`", + content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`", ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="removerole") async def removerole( @@ -418,8 +422,8 @@ class Aurora(commands.Cog): target: discord.Member, role: discord.Role, reason: str, - duration: str = None, - silent: bool = None, + duration: str | None = None, + silent: bool | None = None, ): """Remove a role from a user. @@ -452,7 +456,7 @@ class Aurora(commands.Cog): ) return else: - parsed_time = "NULL" + parsed_time = None if role.id not in addrole_whitelist: await interaction.response.send_message( @@ -478,7 +482,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -499,7 +504,8 @@ class Aurora(commands.Cog): content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`" ) - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "REMOVEROLE", @@ -510,12 +516,10 @@ class Aurora(commands.Cog): reason, ) await response.edit( - content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`", + content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`", ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="mute") async def mute( @@ -524,7 +528,7 @@ class Aurora(commands.Cog): target: discord.Member, duration: str, reason: str, - silent: bool = None, + silent: bool | None = None, ): """Mute a user. @@ -575,7 +579,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -587,31 +592,30 @@ class Aurora(commands.Cog): except discord.errors.HTTPException: pass - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "MUTE", "USER", target.id, - 0, + None, parsed_time, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="unmute") async def unmute( self, interaction: discord.Interaction, target: discord.Member, - reason: str = None, - silent: bool = None, + reason: str | None = None, + silent: bool | None = None, ): """Unmute a user. @@ -651,7 +655,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -662,23 +667,22 @@ class Aurora(commands.Cog): except discord.errors.HTTPException: pass - moderation_id = await mysql_log( + moderation = await Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "UNMUTE", "USER", target.id, - 0, - "NULL", + None, + None, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been unmuted! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been unmuted! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="kick") async def kick( @@ -686,7 +690,7 @@ class Aurora(commands.Cog): interaction: discord.Interaction, target: discord.Member, reason: str, - silent: bool = None, + silent: bool | None = None, ): """Kick a user. @@ -710,7 +714,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -723,23 +728,22 @@ class Aurora(commands.Cog): await target.kick(reason=f"Kicked by {interaction.user.id} for: {reason}") - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "KICK", "USER", target.id, - 0, - "NULL", + None, + None, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been kicked! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been kicked! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="ban") @app_commands.choices( @@ -757,9 +761,9 @@ class Aurora(commands.Cog): interaction: discord.Interaction, target: discord.User, reason: str, - duration: str = None, - delete_messages: Choice[int] = None, - silent: bool = None, + duration: str | None = None, + delete_messages: Choice[int] | None = None, + silent: bool | None = None, ): """Ban a user. @@ -813,7 +817,8 @@ class Aurora(commands.Cog): try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -831,23 +836,22 @@ class Aurora(commands.Cog): delete_message_seconds=delete_messages_seconds, ) - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "TEMPBAN", "USER", target.id, - 0, + None, parsed_time, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been banned for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation_id}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been banned for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation.id}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) else: await interaction.response.send_message( content=f"{target.mention} has been banned!\n**Reason** - `{reason}`" @@ -857,8 +861,9 @@ class Aurora(commands.Cog): silent = not await config.guild(interaction.guild).dm_users() if silent is False: try: - embed = embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + embed = await message_factory( + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -875,7 +880,8 @@ class Aurora(commands.Cog): delete_message_seconds=delete_messages_seconds, ) - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "BAN", @@ -886,20 +892,18 @@ class Aurora(commands.Cog): reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been banned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been banned! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) - - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="unban") async def unban( self, interaction: discord.Interaction, target: discord.User, - reason: str = None, - silent: bool = None, + reason: str | None = None, + silent: bool | None = None, ): """Unban a user. @@ -941,7 +945,8 @@ class Aurora(commands.Cog): if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), + bot=interaction.client, + color=await self.bot.get_embed_color(interaction.channel), guild=interaction.guild, moderator=interaction.user, reason=reason, @@ -952,34 +957,76 @@ class Aurora(commands.Cog): except discord.errors.HTTPException: pass - moderation_id = await mysql_log( + moderation = Moderation.log( + interaction.client, interaction.guild.id, interaction.user.id, "UNBAN", "USER", target.id, - 0, - "NULL", + None, + None, reason, ) await interaction.edit_original_response( - content=f"{target.mention} has been unbanned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" + content=f"{target.mention} has been unbanned! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + @app_commands.command(name="slowmode") + async def slowmode( + self, + interaction: discord.Interaction, + interval: int, + channel: discord.TextChannel | None = None, + reason: str | None = None, + ): + """Set the slowmode of a channel. + + Parameters + ----------- + interval: int + The slowmode interval in seconds + channel: discord.TextChannel + The channel to set the slowmode in + reason: str + Why are you setting the slowmode?""" + if channel is None: + channel = interaction.channel + + if not await check_moddable(channel, interaction, ["manage_channels"]): + return + + await channel.edit(slowmode_delay=interval) + await interaction.response.send_message(f"Slowmode in {channel.mention} has been set to {interval} seconds!\n**Reason** - `{reason}`") + + moderation = Moderation.log( + interaction.client, + interaction.guild.id, + interaction.user.id, + "SLOWMODE", + "CHANNEL", + channel.id, + None, + None, + reason, + metadata={"interval": f"{interval} seconds"} + ) + await interaction.edit_original_response(content=f"Slowmode in {channel.mention} has been set to {interval} seconds! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`") + await log(interaction, moderation.id) + await send_evidenceformat(interaction, moderation.id) @app_commands.command(name="history") async def history( self, interaction: discord.Interaction, - target: discord.User = None, - moderator: discord.User = None, - pagesize: app_commands.Range[int, 1, 20] = None, + target: discord.User | None = None, + moderator: discord.User | None = None, + pagesize: app_commands.Range[int, 1, 20] | None = None, page: int = 1, - ephemeral: bool = None, - inline: bool = None, + ephemeral: bool | None = None, + inline: bool | None = None, export: bool = False, ): """List previous infractions. @@ -1045,21 +1092,6 @@ class Aurora(commands.Cog): database = connect() if export: - database.row_factory = sqlite3.Row - cursor = database.cursor() - - query = f"""SELECT * - FROM moderation_{interaction.guild.id} - ORDER BY moderation_id DESC;""" - cursor.execute(query) - - results = cursor.fetchall() - - cases = [] - for result in results: - case = dict(result) - cases.append(case) - try: filename = ( str(data_manager.cog_data_path(cog_instance=self)) @@ -1067,8 +1099,10 @@ class Aurora(commands.Cog): + f"moderation_{interaction.guild.id}.json" ) + cases = Moderation.get_latest(bot=interaction.client, guild_id=interaction.guild.id) + with open(filename, "w", encoding="utf-8") as f: - json.dump(cases, f, indent=2) + dump(obj=cases, fp=f, indent=2) await interaction.followup.send( file=discord.File( @@ -1086,40 +1120,17 @@ class Aurora(commands.Cog): + box(e, "py"), ephemeral=ephemeral, ) - cursor.close() database.close() return - cursor = database.cursor() - if target: - query = f"""SELECT * - FROM moderation_{interaction.guild.id} - WHERE target_id = ? - ORDER BY moderation_id DESC;""" - cursor.execute(query, (target.id,)) + moderations = Moderation.find_by_target(interaction.client, interaction.guild.id, target.id) elif moderator: - query = f"""SELECT * - FROM moderation_{interaction.guild.id} - WHERE moderator_id = ? - ORDER BY moderation_id DESC;""" - cursor.execute(query, (moderator.id,)) + moderations = Moderation.find_by_moderator(interaction.client, interaction.guild.id, moderator.id) else: - query = f"""SELECT * - FROM moderation_{interaction.guild.id} - ORDER BY moderation_id DESC;""" - cursor.execute(query) + moderations = Moderation.get_latest(interaction.client, interaction.guild.id) - results = cursor.fetchall() - result_dict_list = [] - - for result in results: - case_dict = generate_dict(result) - if case_dict["moderation_id"] == 0: - continue - result_dict_list.append(case_dict) - - case_quantity = len(result_dict_list) + case_quantity = len(moderations) page_quantity = ceil(case_quantity / pagesize) start_index = (page - 1) * pagesize end_index = page * pagesize @@ -1132,74 +1143,44 @@ class Aurora(commands.Cog): memory_dict = {} - for case in result_dict_list[start_index:end_index]: - if case["target_id"] not in memory_dict: - if case["target_type"] == "USER": - memory_dict[str(case["target_id"])] = await fetch_user_dict( - interaction.client, case["target_id"] - ) - elif case["target_type"] == "CHANNEL": - memory_dict[str(case["target_id"])] = await fetch_channel_dict( - interaction.guild, case["target_id"] - ) - target_user = memory_dict[str(case["target_id"])] + for mod in moderations[start_index:end_index]: + if mod.target_id not in memory_dict: + memory_dict.update({ + str(mod.target_id): await mod.get_target() + }) + target = memory_dict[str(mod.target_id)] - if case["target_type"] == "USER": - target_name = ( - f"`{target_user['name']}`" - if target_user["discriminator"] == "0" - else f"`{target_user['name']}#{target_user['discriminator']}`" - ) - elif case["target_type"] == "CHANNEL": - target_name = f"`{target_user['mention']}`" + if mod.moderator_id not in memory_dict: + memory_dict.update({ + str(mod.moderator_id): await mod.get_moderator() + }) + moderator = memory_dict[str(mod.moderator_id)] - if case["moderator_id"] not in memory_dict: - memory_dict[str(case["moderator_id"])] = await fetch_user_dict( - interaction.client, case["moderator_id"] - ) - moderator_user = memory_dict[str(case["moderator_id"])] - moderator_name = ( - f"`{moderator_user['name']}`" - if moderator_user["discriminator"] == "0" - else f"`{moderator_user['name']}#{moderator_user['discriminator']}`" - ) + field_name = f"Case #{mod.id:,} ({str.title(mod.type)})" + field_value = f"**Target:** `{target.name}` ({target.id})\n**Moderator:** `{moderator.name}` ({moderator.id})" - field_name = f"Case #{case['moderation_id']:,} ({str.title(case['moderation_type'])})" - field_value = f"**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})" - - if len(case["reason"]) > 125: - field_value += f"\n**Reason:** `{str(case['reason'])[:125]}...`" + if len(str(mod.reason)) > 125: + field_value += f"\n**Reason:** `{str(mod.reason)[:125]}...`" else: - field_value += f"\n**Reason:** `{str(case['reason'])}`" + field_value += f"\n**Reason:** `{str(mod.reason)}`" - if case["duration"] != "NULL": - td = timedelta( - **{ - unit: int(val) - for unit, val in zip( - ["hours", "minutes", "seconds"], case["duration"].split(":") - ) - } - ) + if mod.duration: duration_embed = ( - f"{humanize_timedelta(timedelta=td)} | " - if bool(case["expired"]) is False - else f"{humanize_timedelta(timedelta=td)} | Expired" + f"{humanize_timedelta(timedelta=mod.duration)} | " + if mod.expired is False + else f"{humanize_timedelta(timedelta=mod.duration)} | Expired" ) field_value += f"\n**Duration:** {duration_embed}" field_value += ( - f"\n**Timestamp:** | " + f"\n**Timestamp:** | " ) - if case["role_id"] != "0": - role = interaction.guild.get_role(int(case["role_id"])) - if role is not None: - field_value += f"\n**Role:** {role.mention}" - else: - field_value += f"\n**Role:** Deleted Role ({case['role_id']})" + if mod.role_id: + role = await mod.get_role() + field_value += f"\n**Role:** {role.mention} ({role.id})" - if bool(case["resolved"]): + if mod.resolved: field_value += "\n**Resolved:** True" embed.add_field(name=field_name, value=field_value, inline=inline) @@ -1208,7 +1189,7 @@ class Aurora(commands.Cog): @app_commands.command(name="resolve") async def resolve( - self, interaction: discord.Interaction, case: int, reason: str = None + self, interaction: discord.Interaction, case: int, reason: str | None = None ): """Resolve a specific case. @@ -1220,7 +1201,7 @@ class Aurora(commands.Cog): Reason for resolving case""" permissions = check_permissions( interaction.client.user, - ["embed_links", "moderate_members", "ban_members"], + ("embed_links", "moderate_members", "ban_members"), interaction, ) if permissions: @@ -1232,39 +1213,14 @@ class Aurora(commands.Cog): ) return - database = connect() - cursor = database.cursor() - - query_1 = ( - f"SELECT * FROM moderation_{interaction.guild.id} WHERE moderation_id = ?;" - ) - cursor.execute(query_1, (case,)) - result_1 = cursor.fetchone() - if result_1 is None or case == 0: + try: + moderation = Moderation.find_by_id(interaction.client, case, interaction.guild.id) + except ValueError: await interaction.response.send_message( - content=error(f"There is no moderation with a case number of {case}."), - ephemeral=True, + content=error(f"Case #{case:,} does not exist!"), ephemeral=True ) return - - query_2 = f"SELECT * FROM moderation_{interaction.guild.id} WHERE moderation_id = ? AND resolved = 0;" - cursor.execute(query_2, (case,)) - result_2 = cursor.fetchone() - if result_2 is None: - await interaction.response.send_message( - content=error( - f"This moderation has already been resolved!\nUse `/case {case}` for more information." - ), - ephemeral=True, - ) - return - - case_dict = generate_dict(result_2) - if reason is None: - reason = "No reason given." - - changes: list = case_dict["changes"] - if len(changes) > 25: + if len(moderation.changes) > 25: await interaction.response.send_message( content=error( "Due to limitations with Discord's embed system, you cannot edit a case more than 25 times." @@ -1272,81 +1228,28 @@ class Aurora(commands.Cog): ephemeral=True, ) return - if not changes: - changes.append( - { - "type": "ORIGINAL", - "timestamp": case_dict["timestamp"], - "reason": case_dict["reason"], - "user_id": case_dict["moderator_id"], - } - ) - changes.append( - { - "type": "RESOLVE", - "timestamp": int(time.time()), - "reason": reason, - "user_id": interaction.user.id, - } - ) - if case_dict["moderation_type"] in ["UNMUTE", "UNBAN"]: - await interaction.response.send_message( - content=error("You cannot resolve this type of moderation!"), - ephemeral=True, - ) - return - - if case_dict["moderation_type"] in ["MUTE", "TEMPBAN", "BAN"]: - if case_dict["moderation_type"] == "MUTE": - try: - member = await interaction.guild.fetch_member( - case_dict["target_id"] - ) - - await member.timeout( - None, reason=f"Case #{case:,} resolved by {interaction.user.id}" - ) - except discord.NotFound: - pass - - if case_dict["moderation_type"] in ["TEMPBAN", "BAN"]: - try: - user = await interaction.client.fetch_user(case_dict["target_id"]) - - await interaction.guild.unban( - user, reason=f"Case #{case} resolved by {interaction.user.id}" - ) - except discord.NotFound: - pass - - resolve_query = f"UPDATE `moderation_{interaction.guild.id}` SET resolved = 1, changes = ?, resolved_by = ?, resolve_reason = ? WHERE moderation_id = ?" - else: - resolve_query = f"UPDATE `moderation_{interaction.guild.id}` SET resolved = 1, changes = ?, resolved_by = ?, resolve_reason = ? WHERE moderation_id = ?" - - cursor.execute( - resolve_query, - ( - json.dumps(changes), - interaction.user.id, - reason, - case_dict["moderation_id"], - ), - ) - database.commit() + try: + await moderation.resolve(interaction.user.id, reason) + except (ValueError, TypeError) as e: + if e == ValueError: + await interaction.response.send_message( + content=error("This case has already been resolved!"), ephemeral=True + ) + elif e == TypeError: + await interaction.response.send_message( + content=error("This case type cannot be resolved!"), ephemeral=True + ) embed = await case_factory( interaction=interaction, - case_dict=await fetch_case(case, interaction.guild.id), + moderation=moderation, ) await interaction.response.send_message( content=f"✅ Moderation #{case:,} resolved!", embed=embed ) await log(interaction, case, resolved=True) - cursor.close() - database.close() - @app_commands.command(name="case") @app_commands.choices( export=[ @@ -1358,10 +1261,10 @@ class Aurora(commands.Cog): self, interaction: discord.Interaction, case: int, - ephemeral: bool = None, + ephemeral: bool | None = None, evidenceformat: bool = False, changes: bool = False, - export: Choice[str] = None, + export: Choice[str] | None = None, ): """Check the details of a specific case. @@ -1394,68 +1297,67 @@ class Aurora(commands.Cog): or False ) - if case != 0: - case_dict = await fetch_case(case, interaction.guild.id) - if case_dict: - if export: - if export.value == "file" or len(str(case_dict)) > 1800: - filename = ( - str(data_manager.cog_data_path(cog_instance=self)) - + str(os.sep) - + f"moderation_{interaction.guild.id}_case_{case}.json" - ) + try: + mod = Moderation.find_by_id(interaction.client, case, interaction.guild.id) + except ValueError: + await interaction.response.send_message( + content=error(f"Case #{case:,} does not exist!"), ephemeral=True + ) + return - with open(filename, "w", encoding="utf-8") as f: - json.dump(case_dict, f, indent=2) + if export: + if export.value == "file" or len(mod.to_json(2)) > 1800: + filename = ( + str(data_manager.cog_data_path(cog_instance=self)) + + str(os.sep) + + f"moderation_{interaction.guild.id}_case_{case}.json" + ) - if export.value == "codeblock": - content = f"Case #{case:,} exported.\n" + warning( - "Case was too large to export as codeblock, so it has been uploaded as a `.json` file." - ) - else: - content = f"Case #{case:,} exported." - - await interaction.response.send_message( - content=content, - file=discord.File( - filename, - f"moderation_{interaction.guild.id}_case_{case}.json", - ), - ephemeral=ephemeral, - ) - - os.remove(filename) - return - await interaction.response.send_message( - content=box(json.dumps(case_dict, indent=2), 'json'), - ephemeral=ephemeral, - ) - return - if changes: - embed = await changes_factory( - interaction=interaction, case_dict=case_dict - ) - await interaction.response.send_message( - embed=embed, ephemeral=ephemeral - ) - elif evidenceformat: - content = await evidenceformat_factory( - interaction=interaction, case_dict=case_dict - ) - await interaction.response.send_message( - content=content, ephemeral=ephemeral + with open(filename, "w", encoding="utf-8") as f: + mod.to_json(2, f) + if export.value == "codeblock": + content = f"Case #{case:,} exported.\n" + warning( + "Case was too large to export as codeblock, so it has been uploaded as a `.json` file." ) else: - embed = await case_factory( - interaction=interaction, case_dict=case_dict - ) - await interaction.response.send_message( - embed=embed, ephemeral=ephemeral - ) + content = f"Case #{case:,} exported." + + await interaction.response.send_message( + content=content, + file=discord.File( + filename, + f"moderation_{interaction.guild.id}_case_{case}.json", + ), + ephemeral=ephemeral, + ) + + os.remove(filename) return - await interaction.response.send_message( - content=f"No case with case number `{case}` found.", ephemeral=True - ) + await interaction.response.send_message( + content=box(mod.to_json(2), 'json'), + ephemeral=ephemeral, + ) + return + if changes: + embed = await changes_factory( + interaction=interaction, moderation=mod + ) + await interaction.response.send_message( + embed=embed, ephemeral=ephemeral + ) + elif evidenceformat: + content = await evidenceformat_factory(moderation=mod) + await interaction.response.send_message( + content=content, ephemeral=ephemeral + ) + else: + embed = await case_factory( + interaction=interaction, moderation=mod + ) + await interaction.response.send_message( + embed=embed, ephemeral=ephemeral + ) + return @app_commands.command(name="edit") async def edit( @@ -1463,7 +1365,7 @@ class Aurora(commands.Cog): interaction: discord.Interaction, case: int, reason: str, - duration: str = None, + duration: str | None = None, ): """Edit the reason of a specific case. @@ -1488,122 +1390,96 @@ class Aurora(commands.Cog): ) return - if case != 0: - parsed_time = None - case_dict = await fetch_case(case, interaction.guild.id) - if case_dict: - if duration: - parsed_time = parse_timedelta(duration) - if parsed_time is None: - await interaction.response.send_message( - error("Please provide a valid duration!"), ephemeral=True - ) - return + try: + moderation = Moderation.find_by_id(interaction.client, case, interaction.guild.id) + old_moderation = moderation + except ValueError: + await interaction.response.send_message( + content=error(f"Case #{case:,} does not exist!"), ephemeral=True + ) + return - end_timestamp = case_dict["timestamp"] + parsed_time.total_seconds() + if len(moderation.changes) > 25: + return await interaction.response.send_message( + content=error( + "Due to limitations with Discord's embed system, you cannot edit a case more than 25 times." + ), + ephemeral=True, + ) - if case_dict["moderation_type"] == "MUTE": - if ( - time.time() - case_dict["timestamp"] - ) + parsed_time.total_seconds() > 2419200: - await interaction.response.send_message( - error( - "Please provide a duration that is less than 28 days from the initial moderation." - ) - ) - return - - try: - member = await interaction.guild.fetch_member( - case_dict["target_id"] - ) - - await member.timeout( - parsed_time, - reason=f"Case #{case:,} edited by {interaction.user.id}", - ) - except discord.NotFound: - pass - - changes: list = case_dict["changes"] - if len(changes) > 25: - await interaction.response.send_message( - content=error( - "Due to limitations with Discord's embed system, you cannot edit a case more than 25 times." - ), - ephemeral=True, - ) - return - if not changes: - changes.append( - { - "type": "ORIGINAL", - "timestamp": case_dict["timestamp"], - "reason": case_dict["reason"], - "user_id": case_dict["moderator_id"], - "duration": case_dict["duration"], - "end_timestamp": case_dict["end_timestamp"], - } - ) - if parsed_time: - changes.append( - { - "type": "EDIT", - "timestamp": int(time.time()), - "reason": reason, - "user_id": interaction.user.id, - "duration": convert_timedelta_to_str(parsed_time), - "end_timestamp": end_timestamp, - } - ) - else: - changes.append( - { - "type": "EDIT", - "timestamp": int(time.time()), - "reason": reason, - "user_id": interaction.user.id, - "duration": case_dict["duration"], - "end_timestamp": case_dict["end_timestamp"], - } - ) - - database = connect() - cursor = database.cursor() - - if parsed_time: - update_query = f"UPDATE `moderation_{interaction.guild.id}` SET changes = ?, reason = ?, duration = ?, end_timestamp = ? WHERE moderation_id = ?" - cursor.execute( - update_query, - ( - json.dumps(changes), - reason, - convert_timedelta_to_str(parsed_time), - end_timestamp, - case, - ), - ) - else: - update_query = f"UPDATE `moderation_{interaction.guild.id}` SET changes = ?, reason = ? WHERE moderation_id = ?" - cursor.execute(update_query, (json.dumps(changes), reason, case)) - database.commit() - - new_case = await fetch_case(case, interaction.guild.id) - embed = await case_factory(interaction=interaction, case_dict=new_case) - - await interaction.response.send_message( - content=f"✅ Moderation #{case:,} edited!", - embed=embed, - ephemeral=True, + if duration: + moderation.duration = parse_timedelta(duration) + if moderation.duration is None: + return await interaction.response.send_message( + error("Please provide a valid duration!"), ephemeral=True ) - await log(interaction, case) - cursor.close() - database.close() - return + moderation.end_timestamp = moderation.timestamp + moderation.duration.total_seconds() + + if moderation.type == "MUTE": + if ( + time.time() - moderation.unix_timestamp + ) + moderation.duration.total_seconds() > 2419200: + return await interaction.response.send_message( + error( + "Please provide a duration that is less than 28 days from the initial moderation." + ) + ) + + try: + member = await interaction.guild.fetch_member( + moderation.target_id + ) + + await member.timeout( + moderation.duration, + reason=f"Case #{case:,} edited by {interaction.user.id}", + ) + except discord.NotFound: + pass + + if reason: + moderation.reason = reason + + if not moderation.changes: + moderation.changes.append(Change.from_dict(interaction.client, { + "type": "ORIGINAL", + "timestamp": old_moderation.timestamp, + "reason": old_moderation.reason, + "user_id": old_moderation.moderator_id, + "duration": old_moderation.duration, + "end_timestamp": old_moderation.end_timestamp, + })) + if duration: + moderation.changes.append(Change.from_dict(interaction.client, { + "type": "EDIT", + "timestamp": int(time.time()), + "reason": reason, + "user_id": interaction.user.id, + "duration": moderation.duration, + "end_timestamp": moderation.end_timestamp, + })) + else: + moderation.changes.append(Change.from_dict(interaction.client, { + "type": "EDIT", + "timestamp": int(time.time()), + "reason": reason, + "user_id": interaction.user.id, + "duration": moderation.duration, + "end_timestamp": moderation.end_timestamp, + })) + + moderation.update() + embed = await case_factory(interaction=interaction, moderation=moderation) + await interaction.response.send_message( - content=error(f"No case with case number `{case}` found."), ephemeral=True + content=f"✅ Moderation #{case:,} edited!", + embed=embed, + ephemeral=True, ) + await log(interaction, case) + + return @tasks.loop(minutes=1) async def handle_expiry(self): @@ -1620,7 +1496,7 @@ class Aurora(commands.Cog): if not await self.bot.cog_disabled_in_guild(self, guild): time_per_guild = time.time() - tempban_query = f"SELECT target_id, moderation_id FROM moderation_{guild.id} WHERE end_timestamp != 0 AND end_timestamp <= ? AND moderation_type = 'TEMPBAN' AND expired = 0" + tempban_query = f"SELECT target_id, moderation_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL 0 AND end_timestamp <= ? AND moderation_type = 'TEMPBAN' AND expired = 0" try: cursor.execute(tempban_query, (time.time(),)) @@ -1645,7 +1521,8 @@ class Aurora(commands.Cog): ) embed = await message_factory( - await self.bot.get_embed_color(guild.channels[0]), + bot=self.bot, + color=await self.bot.get_embed_color(guild.channels[0]), guild=guild, reason=f"Automatic unban from case #{moderation_id}", moderation_type="unbanned", @@ -1679,7 +1556,7 @@ class Aurora(commands.Cog): ) removerole_num = 0 - addrole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp != 0 AND end_timestamp <= ? AND moderation_type = 'ADDROLE' AND expired = 0" + addrole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL AND end_timestamp <= ? AND moderation_type = 'ADDROLE' AND expired = 0" try: cursor.execute(addrole_query, (time.time(),)) result = cursor.fetchall() @@ -1714,7 +1591,7 @@ class Aurora(commands.Cog): continue addrole_num = 0 - removerole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp != 0 AND end_timestamp <= ? AND moderation_type = 'REMOVEROLE' AND expired = 0" + removerole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL 0 AND end_timestamp <= ? AND moderation_type = 'REMOVEROLE' AND expired = 0" try: cursor.execute(removerole_query, (time.time(),)) result = cursor.fetchall() @@ -1743,7 +1620,7 @@ class Aurora(commands.Cog): logger.error("Adding the role %s to user %s failed due to: \n%s", role_id, target_id, e) continue - expiry_query = f"UPDATE `moderation_{guild.id}` SET expired = 1 WHERE (end_timestamp != 0 AND end_timestamp <= ? AND expired = 0) OR (expired = 0 AND resolved = 1);" + expiry_query = f"UPDATE `moderation_{guild.id}` SET expired = 1 WHERE (end_timestamp IS NOT NULL AND end_timestamp <= ? AND expired = 0) OR (expired = 0 AND resolved = 1);" cursor.execute(expiry_query, (time.time(),)) per_guild_completion_time = (time.time() - time_per_guild) * 1000 diff --git a/aurora/importers/aurora.py b/aurora/importers/aurora.py index 44cab98..87091bd 100644 --- a/aurora/importers/aurora.py +++ b/aurora/importers/aurora.py @@ -1,13 +1,15 @@ # pylint: disable=duplicate-code import json -from datetime import timedelta +from time import time from typing import Dict from discord import ButtonStyle, Interaction, Message, ui from redbot.core import commands from redbot.core.utils.chat_formatting import box, warning -from ..utilities.database import connect, create_guild_table, mysql_log +from ..models.moderation import Moderation +from ..utilities.database import connect, create_guild_table +from ..utilities.utils import timedelta_from_string class ImportAuroraView(ui.View): @@ -62,10 +64,23 @@ class ImportAuroraView(ui.View): case["target_type"] = "USER" if "role_id" not in case or not case["role_id"]: - case["role_id"] = 0 + case["role_id"] = None + else: + case["role_id"] = int(case["role_id"]) + + case["target_id"] = int(case["target_id"]) + case["moderator_id"] = int(case["moderator_id"]) if "changes" not in case or not case["changes"]: - case["changes"] = [] + changes = [] + else: + changes = json.loads(case["changes"]) + if isinstance(changes, str): + changes: list[dict] = json.loads(changes) + + for change in changes: + if change.get("bot"): + del change["bot"] if "metadata" not in case: metadata = {} @@ -73,31 +88,36 @@ class ImportAuroraView(ui.View): metadata: Dict[str, any] = json.loads(case["metadata"]) if not metadata.get("imported_from"): metadata.update({"imported_from": "Aurora"}) + metadata.update({"imported_timestamp": int(time())}) - if case["duration"] != "NULL": - hours, minutes, seconds = map(int, case["duration"].split(":")) - duration = timedelta(hours=hours, minutes=minutes, seconds=seconds) + if case["duration"] != "NULL" and case["duration"] is not None: + duration = timedelta_from_string(case["duration"]) else: - duration = "NULL" + duration = None - await mysql_log( - self.ctx.guild.id, - case["moderator_id"], - case["moderation_type"], - case["target_type"], - case["target_id"], - case["role_id"], - duration, - case["reason"], - timestamp=case["timestamp"], - resolved=case["resolved"], - resolved_by=case["resolved_by"], - resolved_reason=case["resolve_reason"], - expired=case["expired"], - changes=case["changes"], - metadata=metadata, - database=database, - ) + try: + Moderation.log( + bot=interaction.client, + guild_id=self.ctx.guild.id, + moderator_id=case["moderator_id"], + moderation_type=case["moderation_type"], + target_type=case["target_type"], + target_id=case["target_id"], + role_id=case["role_id"], + duration=duration, + reason=case["reason"], + timestamp=case["timestamp"], + resolved=case["resolved"], + resolved_by=case["resolved_by"], + resolved_reason=case["resolve_reason"], + expired=case["expired"], + changes=changes, + metadata=metadata, + database=database, + return_obj=False + ) + except Exception as e: # pylint: disable=broad-exception-caught + failed_cases.append(str(case["moderation_id"]) + f": {e}") await interaction.edit_original_response(content="Import complete.") if failed_cases: diff --git a/aurora/importers/galacticbot.py b/aurora/importers/galacticbot.py index c6e0b99..a7c2d27 100644 --- a/aurora/importers/galacticbot.py +++ b/aurora/importers/galacticbot.py @@ -1,12 +1,14 @@ # pylint: disable=duplicate-code import json from datetime import timedelta +from time import time from discord import ButtonStyle, Interaction, Message, ui from redbot.core import commands from redbot.core.utils.chat_formatting import box, warning -from ..utilities.database import connect, create_guild_table, mysql_log +from ..models.moderation import Change, Moderation +from ..utilities.database import connect, create_guild_table class ImportGalacticBotView(ui.View): @@ -67,12 +69,12 @@ class ImportGalacticBotView(ui.View): if case["duration"] is not None and float(case["duration"]) != 0: duration = timedelta(seconds=round(float(case["duration"]) / 1000)) else: - duration = "NULL" + duration = None except OverflowError: failed_cases.append(case["case"]) continue - metadata = {"imported_from": "GalacticBot"} + metadata = {"imported_from": "GalacticBot", "imported_timestamp": int(time())} if case["type"] == "SLOWMODE": metadata["seconds"] = case["data"]["seconds"] @@ -98,37 +100,37 @@ class ImportGalacticBotView(ui.View): if resolved_timestamp is None: resolved_timestamp = timestamp changes = [ - { + Change.from_dict(interaction.client, { "type": "ORIGINAL", "reason": case["reason"], "user_id": case["executor"], "timestamp": timestamp, - }, - { + }), + Change.from_dict(interaction.client, { "type": "RESOLVE", "reason": resolved_reason, "user_id": resolved_by, "timestamp": resolved_timestamp, - }, + }), ] else: - resolved = 0 - resolved_by = "NULL" - resolved_reason = "NULL" - changes = [] + resolved = None + resolved_by = None + resolved_reason = None + changes = None if case["reason"] and case["reason"] != "N/A": reason = case["reason"] else: - reason = "NULL" + reason = None - await mysql_log( + Moderation.log( self.ctx.guild.id, case["executor"], case["type"], case["targetType"], case["target"], - 0, + None, duration, reason, timestamp=timestamp, diff --git a/aurora/info.json b/aurora/info.json index 52a577e..d232f7e 100644 --- a/aurora/info.json +++ b/aurora/info.json @@ -9,6 +9,7 @@ "disabled": false, "min_bot_version": "3.5.0", "min_python_version": [3, 10, 0], + "requirements": ["pydantic"], "tags": [ "mod", "moderate", diff --git a/aurora/menus/addrole.py b/aurora/menus/addrole.py index 11f6b32..36c9056 100644 --- a/aurora/menus/addrole.py +++ b/aurora/menus/addrole.py @@ -2,12 +2,12 @@ from discord import ButtonStyle, Interaction, Message, ui from redbot.core import commands from redbot.core.utils.chat_formatting import error -from aurora.utilities.config import config -from aurora.utilities.factory import addrole_embed +from ..utilities.config import config +from ..utilities.factory import addrole_embed class Addrole(ui.View): - def __init__(self, ctx: commands.Context, message: Message, timeout: int = None): + def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None): super().__init__() self.ctx = ctx self.message = message diff --git a/aurora/menus/guild.py b/aurora/menus/guild.py index f99e552..71186b7 100644 --- a/aurora/menus/guild.py +++ b/aurora/menus/guild.py @@ -1,13 +1,13 @@ from discord import ButtonStyle, Interaction, Message, ui from redbot.core import commands -from aurora.utilities.config import config -from aurora.utilities.factory import guild_embed -from aurora.utilities.utils import create_pagesize_options +from ..utilities.config import config +from ..utilities.factory import guild_embed +from ..utilities.utils import create_pagesize_options class Guild(ui.View): - def __init__(self, ctx: commands.Context, message: Message, timeout: int = None): + def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None): super().__init__() self.ctx = ctx self.message = message diff --git a/aurora/menus/immune.py b/aurora/menus/immune.py index 2a5c007..dd710b1 100644 --- a/aurora/menus/immune.py +++ b/aurora/menus/immune.py @@ -2,12 +2,12 @@ from discord import ButtonStyle, Interaction, Message, ui from redbot.core import commands from redbot.core.utils.chat_formatting import error -from aurora.utilities.config import config -from aurora.utilities.factory import immune_embed +from ..utilities.config import config +from ..utilities.factory import immune_embed class Immune(ui.View): - def __init__(self, ctx: commands.Context, message: Message, timeout: int = None): + def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None): super().__init__() self.ctx = ctx self.message = message diff --git a/aurora/menus/overrides.py b/aurora/menus/overrides.py index c5d0e68..4327131 100644 --- a/aurora/menus/overrides.py +++ b/aurora/menus/overrides.py @@ -1,13 +1,13 @@ from discord import ButtonStyle, Interaction, Message, ui from redbot.core import commands -from aurora.utilities.config import config -from aurora.utilities.factory import overrides_embed -from aurora.utilities.utils import create_pagesize_options +from ..utilities.config import config +from ..utilities.factory import overrides_embed +from ..utilities.utils import create_pagesize_options class Overrides(ui.View): - def __init__(self, ctx: commands.Context, message: Message, timeout: int = None): + def __init__(self, ctx: commands.Context, message: Message, timeout: int | None = None): super().__init__() self.ctx = ctx self.message = message diff --git a/aurora/models/__init__.py b/aurora/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aurora/models/base.py b/aurora/models/base.py new file mode 100644 index 0000000..e6005ee --- /dev/null +++ b/aurora/models/base.py @@ -0,0 +1,28 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict +from redbot.core.bot import Red + + +class AuroraBaseModel(BaseModel): + """Base class for all models in Aurora.""" + model_config = ConfigDict(ignored_types=(Red,), arbitrary_types_allowed=True) + bot: Red + + def dump(self) -> dict: + return self.model_dump(exclude={"bot"}) + + def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs): + from ..utilities.json import dump, dumps # pylint: disable=cyclic-import + return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.model_dump(exclude={"bot"}), indent=indent, **kwargs) + +class AuroraGuildModel(AuroraBaseModel): + """Subclass of AuroraBaseModel that includes a guild_id attribute, and a modified to_json() method to match.""" + guild_id: int + + def dump(self) -> dict: + return self.model_dump(exclude={"bot", "guild_id"}) + + def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs): + from ..utilities.json import dump, dumps # pylint: disable=cyclic-import + return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.model_dump(exclude={"bot", "guild_id"}), indent=indent, **kwargs) diff --git a/aurora/models/change.py b/aurora/models/change.py new file mode 100644 index 0000000..22ac222 --- /dev/null +++ b/aurora/models/change.py @@ -0,0 +1,63 @@ +import json +from datetime import datetime, timedelta +from typing import Literal, Optional + +from redbot.core.bot import Red + +from ..utilities.utils import timedelta_from_string +from .base import AuroraBaseModel +from .partials import PartialUser + + +class Change(AuroraBaseModel): + type: Literal["ORIGINAL", "RESOLVE", "EDIT"] + timestamp: datetime + user_id: int + reason: Optional[str] = "No reason provided" + duration: Optional[timedelta] = None + end_timestamp: Optional[datetime] = None + + @property + def unix_timestamp(self) -> int: + return int(self.timestamp.timestamp()) + + def __str__(self): + return f"{self.type} {self.user_id} {self.reason}" + + async def get_user(self) -> "PartialUser": + return await PartialUser.from_id(self.bot, self.user_id) + + @classmethod + def from_dict(cls, bot: Red, data: dict) -> "Change": + if isinstance(data, str): + data = json.loads(data) + if "duration" in data and data["duration"] and not isinstance(data["duration"], timedelta) and not data["duration"] == "NULL": + duration = timedelta_from_string(data["duration"]) + elif "duration" in data and isinstance(data["duration"], timedelta): + duration = data["duration"] + else: + duration = None + + if "end_timestamp" in data and data["end_timestamp"] and not isinstance(data["end_timestamp"], datetime): + end_timestamp = datetime.fromtimestamp(data["end_timestamp"]) + elif "end_timestamp" in data and isinstance(data["end_timestamp"], datetime): + end_timestamp = data["end_timestamp"] + else: + end_timestamp = None + + if not isinstance(data["timestamp"], datetime): + timestamp = datetime.fromtimestamp(data["timestamp"]) + else: + timestamp = data["timestamp"] + + try: + data["user_id"] = int(data["user_id"]) + except ValueError: + data["user_id"] = 0 + + data.update({ + "timestamp": timestamp, + "end_timestamp": end_timestamp, + "duration": duration + }) + return cls(bot=bot, **data) diff --git a/aurora/models/moderation.py b/aurora/models/moderation.py new file mode 100644 index 0000000..f527fff --- /dev/null +++ b/aurora/models/moderation.py @@ -0,0 +1,396 @@ +import json +import sqlite3 +from datetime import datetime, timedelta +from sqlite3 import Cursor +from time import time +from typing import Dict, Iterable, List, Optional, Tuple, Union + +import discord +from discord import NotFound +from redbot.core.bot import Red + +from ..utilities.logger import logger +from ..utilities.utils import timedelta_to_string +from .base import AuroraGuildModel +from .change import Change +from .partials import PartialChannel, PartialRole, PartialUser + + +class Moderation(AuroraGuildModel): + moderation_id: int + timestamp: datetime + moderation_type: str + target_type: str + target_id: int + moderator_id: int + role_id: Optional[int] = None + duration: Optional[timedelta] = None + end_timestamp: Optional[datetime] = None + reason: Optional[str] = None + resolved: bool + resolved_by: Optional[int] = None + resolve_reason: Optional[str] = None + expired: bool + changes: List["Change"] + metadata: Dict + + @property + def id(self) -> int: + return self.moderation_id + + @property + def type(self) -> str: + return self.moderation_type + + @property + def unix_timestamp(self) -> int: + return int(self.timestamp.timestamp()) + + async def get_moderator(self) -> "PartialUser": + return await PartialUser.from_id(self.bot, self.moderator_id) + + async def get_target(self) -> Union["PartialUser", "PartialChannel"]: + if self.target_type == "USER": + return await PartialUser.from_id(self.bot, self.target_id) + return await PartialChannel.from_id(self.bot, self.target_id) + + async def get_resolved_by(self) -> Optional["PartialUser"]: + if self.resolved_by: + return await PartialUser.from_id(self.bot, self.resolved_by) + return None + + async def get_role(self) -> Optional["PartialRole"]: + if self.role_id: + return await PartialRole.from_id(self.bot, self.guild_id, self.role_id) + return None + + def __str__(self) -> str: + return f"{self.moderation_type} {self.target_type} {self.target_id} {self.reason}" + + def __int__(self) -> int: + return self.moderation_id + + async def resolve(self, resolved_by: int, reason: str) -> None: + if self.resolved: + raise ValueError("Case is already resolved!") + + self.resolved = True + self.resolved_by = resolved_by + self.resolve_reason = reason + + if self.type in ["UNMUTE", "UNBAN"]: + raise TypeError("Cannot resolve an unmute or unban case!") + + if self.type == "MUTE": + try: + guild: discord.Guild = await self.bot.fetch_guild(self.guild_id) + member = await guild.fetch_member(self.target_id) + + await member.timeout( + None, reason=f"Case {self.moderation_id} resolved by {resolved_by}{' for '+ reason if reason else ''}" + ) + except NotFound: + pass + + if self.type in ["BAN", "TEMPBAN"]: + try: + guild: discord.Guild = await self.bot.fetch_guild(self.guild_id) + await guild.unban(await self.get_target(), reason=f"Case {self.moderation_id} resolved by {resolved_by}{' for '+ reason if reason else ''}") + except NotFound: + pass + + if not self.changes: + self.changes.append(Change.from_dict(self.bot, { + "type": "ORIGINAL", + "timestamp": self.timestamp, + "reason": self.reason, + "user_id": self.moderator_id, + "duration": self.duration, + "end_timestamp": self.end_timestamp, + })) + self.changes.append(Change.from_dict(self.bot, { + "type": "RESOLVE", + "timestamp": datetime.now(), + "reason": reason, + "user_id": resolved_by, + })) + + self.update() + + def update(self) -> None: + from ..utilities.database import connect + from ..utilities.json import dumps + query = f"UPDATE moderation_{self.guild_id} SET timestamp = ?, moderation_type = ?, target_type = ?, moderator_id = ?, role_id = ?, duration = ?, end_timestamp = ?, reason = ?, resolved = ?, resolved_by = ?, resolve_reason = ?, expired = ?, changes = ?, metadata = ? WHERE moderation_id = ?;" + + with connect() as database: + database.execute(query, ( + self.timestamp.timestamp(), + self.moderation_type, + self.target_type, + self.moderator_id, + self.role_id, + str(self.duration) if self.duration else None, + self.end_timestamp.timestamp() if self.end_timestamp else None, + self.reason, + self.resolved, + self.resolved_by, + self.resolve_reason, + self.expired, + dumps(self.changes), + dumps(self.metadata), + self.moderation_id, + )) + + logger.debug("Row updated in moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s", + self.moderation_id, + self.guild_id, + self.timestamp.timestamp(), + self.moderation_type, + self.target_type, + self.moderator_id, + self.role_id, + str(self.duration) if self.duration else None, + self.end_timestamp.timestamp() if self.end_timestamp else None, + self.reason, + self.resolved, + self.resolved_by, + self.resolve_reason, + self.expired, + dumps(self.changes), + dumps(self.metadata), + ) + + @classmethod + def from_dict(cls, bot: Red, data: dict) -> "Moderation": + return cls(bot=bot, **data) + + @classmethod + def from_result(cls, bot: Red, result: Iterable, guild_id: int) -> "Moderation": + if result[7] is not None and result[7] != "NULL": + hours, minutes, seconds = map(int, result[7].split(':')) + duration = timedelta(hours=hours, minutes=minutes, seconds=seconds) + else: + duration = None + + if result[14] is not None: + changes = json.loads(result[14]) + change_obj_list = [] + if changes: + for change in changes: + change_obj_list.append(Change.from_dict(bot=bot, data=change)) + + if result[15] is not None: + metadata = json.loads(result[15]) + else: + metadata = {} + + case = { + "moderation_id": int(result[0]), + "guild_id": int(guild_id), + "timestamp": datetime.fromtimestamp(result[1]), + "moderation_type": str(result[2]), + "target_type": str(result[3]), + "target_id": int(result[4]), + "moderator_id": int(result[5]), + "role_id": int(result[6]) if result[6] is not None else None, + "duration": duration, + "end_timestamp": datetime.fromtimestamp(result[8]) if result[8] is not None else None, + "reason": result[9], + "resolved": bool(result[10]), + "resolved_by": result[11], + "resolve_reason": result[12], + "expired": bool(result[13]), + "changes": change_obj_list, + "metadata": metadata if metadata else {}, + } + return cls.from_dict(bot=bot, data=case) + + @classmethod + def execute(cls, bot: Red, guild_id: int, query: str, parameters: tuple | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: + from ..utilities.database import connect + logger.trace("Executing query: %s", query) + logger.trace("With parameters: %s", parameters) + if not parameters: + parameters = () + if not cursor: + no_cursor = True + database = connect() + cursor = database.cursor() + else: + no_cursor = False + + cursor.execute(query, parameters) + results = cursor.fetchall() + if no_cursor: + cursor.close() + database.close() + + if results: + cases = [] + for result in results: + case = cls.from_result(bot=bot, result=result, guild_id=guild_id) + if case.moderation_id != 0: + cases.append(case) + return tuple(cases) + return () + + @classmethod + def get_latest(cls, bot: Red, guild_id: int, limit: int | None = None, offset: int = 0, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: + params = [] + query = f"SELECT * FROM moderation_{guild_id} ORDER BY moderation_id DESC" + if types: + query += f" WHERE moderation_type IN ({', '.join(['?' for _ in types])})" + params.extend(types) + if limit: + query += " LIMIT ? OFFSET ?" + params.extend((limit, offset)) + query += ";" + return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=tuple(params) if params else (), cursor=cursor) + + @classmethod + def get_next_case_number(cls, bot: Red, guild_id: int, cursor: Cursor | None = None) -> int: + result = cls.get_latest(bot=bot, guild_id=guild_id, cursor=cursor, limit=1) + return (result[0].moderation_id + 1) if result else 1 + + @classmethod + def find_by_id(cls, bot: Red, moderation_id: int, guild_id: int, cursor: Cursor | None = None) -> "Moderation": + query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;" + case = cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderation_id,), cursor=cursor) + if case: + return case[0] + raise ValueError(f"Case {moderation_id} not found in moderation_{guild_id}!") + + @classmethod + def find_by_target(cls, bot: Red, guild_id: int, target: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: + query = f"SELECT * FROM moderation_{guild_id} WHERE target_id = ?" + if types: + query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})" + query += " ORDER BY moderation_id DESC;" + + return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(target, *types) if types else (target,), cursor=cursor) + + @classmethod + def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: + query = f"SELECT * FROM moderation_{guild_id} WHERE moderator_id = ?" + if types: + query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})" + query += " ORDER BY moderation_id DESC;" + + return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderator, *types) if types else (moderator,), cursor=cursor) + + @classmethod + def log( + cls, + bot: Red, + guild_id: int, + moderator_id: int, + moderation_type: str, + target_type: str, + target_id: int, + role_id: int | None = None, + duration: timedelta | None = None, + reason: str | None = None, + database: sqlite3.Connection | None = None, + timestamp: datetime | None = None, + resolved: bool = False, + resolved_by: int | None = None, + resolved_reason: str | None = None, + expired: bool | None = None, + changes: list | None = None, + metadata: dict | None = None, + return_obj: bool = True, + ) -> Union["Moderation", int]: + from ..utilities.database import connect + from ..utilities.json import dumps + if not timestamp: + timestamp = datetime.fromtimestamp(time()) + elif not isinstance(timestamp, datetime): + timestamp = datetime.fromtimestamp(timestamp) + + if duration == "NULL": + duration = None + + if duration is not None: + end_timestamp = timestamp + duration + else: + duration = None + end_timestamp = None + + if not expired: + if end_timestamp: + expired = bool(timestamp > end_timestamp) + else: + expired = False + + if reason == "NULL": + reason = None + + if resolved_by in ["NULL", "?"]: + resolved_by = None + + if resolved_reason == "NULL": + resolved_reason = None + + if role_id == 0: + role_id = None + + if not database: + database = connect() + close_db = True + else: + close_db = False + cursor = database.cursor() + + moderation_id = cls.get_next_case_number(bot=bot, guild_id=guild_id, cursor=cursor) + + case = { + "moderation_id": moderation_id, + "timestamp": timestamp.timestamp(), + "moderation_type": moderation_type, + "target_type": target_type, + "target_id": target_id, + "moderator_id": moderator_id, + "role_id": role_id, + "duration": timedelta_to_string(duration) if duration else None, + "end_timestamp": end_timestamp.timestamp() if end_timestamp else None, + "reason": reason, + "resolved": resolved, + "resolved_by": resolved_by, + "resolve_reason": resolved_reason, + "expired": expired, + "changes": dumps(changes), + "metadata": dumps(metadata) + } + + sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + cursor.execute(sql, tuple(case.values())) + + cursor.close() + database.commit() + if close_db: + database.close() + + logger.debug( + "Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s", + guild_id, + case["moderation_id"], + case["timestamp"], + case["moderation_type"], + case["target_type"], + case["target_id"], + case["moderator_id"], + case["role_id"], + case["duration"], + case["end_timestamp"], + case["reason"], + case["resolved"], + case["resolved_by"], + case["resolve_reason"], + case["expired"], + case["changes"], + case["metadata"], + ) + + if return_obj: + return cls.find_by_id(bot=bot, moderation_id=moderation_id, guild_id=guild_id) + return moderation_id diff --git a/aurora/models/partials.py b/aurora/models/partials.py new file mode 100644 index 0000000..48f4030 --- /dev/null +++ b/aurora/models/partials.py @@ -0,0 +1,79 @@ +from discord import Forbidden, HTTPException, InvalidData, NotFound +from redbot.core.bot import Red + +from .base import AuroraBaseModel, AuroraGuildModel + + +class PartialUser(AuroraBaseModel): + id: int + username: str + discriminator: int + + @property + def name(self): + return f"{self.username}#{self.discriminator}" if self.discriminator != 0 else self.username + + def __str__(self): + return self.name + + @classmethod + async def from_id(cls, bot: Red, user_id: int) -> "PartialUser": + user = bot.get_user(user_id) + if not user: + try: + user = await bot.fetch_user(user_id) + return cls(bot=bot, id=user.id, username=user.name, discriminator=user.discriminator) + except NotFound: + return cls(bot=bot, id=user_id, username="Deleted User", discriminator=0) + return cls(bot=bot, id=user.id, username=user.name, discriminator=user.discriminator) + + +class PartialChannel(AuroraGuildModel): + id: int + name: str + + @property + def mention(self): + if self.name in ["Deleted Channel", "Forbidden Channel"]: + return self.name + return f"<#{self.id}>" + + def __str__(self): + return self.mention + + @classmethod + async def from_id(cls, bot: Red, channel_id: int) -> "PartialChannel": + channel = bot.get_channel(channel_id) + if not channel: + try: + channel = await bot.fetch_channel(channel_id) + return cls(bot=bot, guild_id=channel.guild.id, id=channel.id, name=channel.name) + except (NotFound, InvalidData, HTTPException, Forbidden) as e: + if e == Forbidden: + return cls(bot=bot, guild_id=0, id=channel_id, name="Forbidden Channel") + return cls(bot=bot, guild_id=0, id=channel_id, name="Deleted Channel") + return cls(bot=bot, guild_id=channel.guild.id, id=channel.id, name=channel.name) + +class PartialRole(AuroraGuildModel): + id: int + name: str + + @property + def mention(self): + if self.name in ["Deleted Role", "Forbidden Role"]: + return self.name + return f"<@&{self.id}>" + + def __str__(self): + return self.mention + + @classmethod + async def from_id(cls, bot: Red, guild_id: int, role_id: int) -> "PartialRole": + try: + guild = await bot.fetch_guild(guild_id, with_counts=False) + except (Forbidden, HTTPException): + return cls(bot=bot, guild_id=guild_id, id=role_id, name="Forbidden Role") + role = guild.get_role(role_id) + if not role: + return cls(bot=bot, guild_id=guild_id, id=role_id, name="Deleted Role") + return cls(bot=bot, guild_id=guild_id, id=role.id, name=role.name) diff --git a/aurora/utilities/database.py b/aurora/utilities/database.py index a2bcb92..588ad35 100644 --- a/aurora/utilities/database.py +++ b/aurora/utilities/database.py @@ -1,15 +1,11 @@ # pylint: disable=cyclic-import import json import sqlite3 -import time -from datetime import datetime, timedelta from discord import Guild from redbot.core import data_manager from .logger import logger -from .utils import (convert_timedelta_to_str, generate_dict, - get_next_case_number) def connect() -> sqlite3.Connection: @@ -42,9 +38,9 @@ async def create_guild_table(guild: Guild): timestamp INTEGER NOT NULL, moderation_type TEXT NOT NULL, target_type TEXT NOT NULL, - target_id TEXT NOT NULL, - moderator_id TEXT NOT NULL, - role_id TEXT, + target_id INTEGER NOT NULL, + moderator_id INTEGER NOT NULL, + role_id INTEGER, duration TEXT, end_timestamp INTEGER, reason TEXT, @@ -52,8 +48,8 @@ async def create_guild_table(guild: Guild): resolved_by TEXT, resolve_reason TEXT, expired INTEGER NOT NULL, - changes TEXT NOT NULL, - metadata TEXT NOT NULL + changes JSON NOT NULL, + metadata JSON NOT NULL ) """ cursor.execute(query) @@ -79,13 +75,13 @@ async def create_guild_table(guild: Guild): "NULL", 0, 0, + None, + None, + None, + None, 0, - "NULL", - 0, - "NULL", - 0, - "NULL", - "NULL", + None, + None, 0, json.dumps([]), json.dumps({}), @@ -102,118 +98,3 @@ async def create_guild_table(guild: Guild): ) database.close() - - -async def mysql_log( - guild_id: str, - author_id: str, - moderation_type: str, - target_type: str, - target_id: int, - role_id: int, - duration: timedelta, - reason: str, - database: sqlite3.Connection = None, - timestamp: int = None, - resolved: bool = False, - resolved_by: str = None, - resolved_reason: str = None, - expired: bool = None, - changes: list = None, - metadata: dict = None, -) -> int: - if not timestamp: - timestamp = int(time.time()) - - if duration != "NULL": - end_timedelta = datetime.fromtimestamp(timestamp) + duration - end_timestamp = int(end_timedelta.timestamp()) - - duration = convert_timedelta_to_str(duration) - else: - end_timestamp = 0 - - if not expired: - if int(time.time()) > end_timestamp: - expired = 1 - else: - expired = 0 - - if resolved_by is None: - resolved_by = "NULL" - - if resolved_reason is None: - resolved_reason = "NULL" - - if not database: - database = connect() - close_db = True - else: - close_db = False - cursor = database.cursor() - - moderation_id = await get_next_case_number(guild_id=guild_id, cursor=cursor) - - sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - val = ( - moderation_id, - timestamp, - moderation_type, - target_type, - target_id, - author_id, - role_id, - duration, - end_timestamp, - reason, - int(resolved), - resolved_by, - resolved_reason, - expired, - json.dumps(changes if changes else []), - json.dumps(metadata if metadata else {}), - ) - cursor.execute(sql, val) - - cursor.close() - database.commit() - if close_db: - database.close() - - logger.debug( - "Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s", - guild_id, - moderation_id, - timestamp, - moderation_type, - target_type, - target_id, - author_id, - role_id, - duration, - end_timestamp, - reason, - int(resolved), - resolved_by, - resolved_reason, - expired, - changes, - metadata, - ) - - return moderation_id - - -async def fetch_case(moderation_id: int, guild_id: str) -> dict: - """This method fetches a case from the database and returns the case's dictionary.""" - database = connect() - cursor = database.cursor() - - query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;" - cursor.execute(query, (moderation_id,)) - result = cursor.fetchone() - - cursor.close() - database.close() - - return generate_dict(result) diff --git a/aurora/utilities/factory.py b/aurora/utilities/factory.py index 0d8a8cd..491f8c3 100644 --- a/aurora/utilities/factory.py +++ b/aurora/utilities/factory.py @@ -4,25 +4,30 @@ from typing import Union from discord import Color, Embed, Guild, Interaction, InteractionMessage, Member, Role, User from redbot.core import commands +from redbot.core.bot import Red from redbot.core.utils.chat_formatting import bold, box, error, humanize_timedelta, warning -from aurora.utilities.config import config -from aurora.utilities.utils import fetch_channel_dict, fetch_user_dict, get_bool_emoji, get_next_case_number, get_pagesize_str +from ..models.moderation import Moderation +from ..models.partials import PartialUser +from .config import config +from .utils import get_bool_emoji, get_pagesize_str async def message_factory( + bot: Red, color: Color, guild: Guild, reason: str, moderation_type: str, - moderator: Union[Member, User] = None, - duration: timedelta = None, - response: InteractionMessage = None, - role: Role = None, + moderator: Union[Member, User] | None = None, + duration: timedelta | None = None, + response: InteractionMessage | None = None, + role: Role | None = None, ) -> Embed: """This function creates a message from set parameters, meant for contacting the moderated user. Args: + bot (Red): The bot instance. color (Color): The color of the embed. guild (Guild): The guild the moderation occurred in. reason (str): The reason for the moderation. @@ -86,7 +91,7 @@ async def message_factory( embed.set_author(name=guild.name) embed.set_footer( - text=f"Case #{await get_next_case_number(guild.id):,}", + text=f"Case #{Moderation.get_next_case_number(bot=bot, guild_id=guild.id):,}", icon_url="attachment://arrow.png", ) @@ -94,256 +99,173 @@ async def message_factory( async def log_factory( - interaction: Interaction, case_dict: dict, resolved: bool = False + interaction: Interaction, moderation: Moderation, resolved: bool = False ) -> Embed: """This function creates a log embed from set parameters, meant for moderation logging. Args: - interaction (Interaction): The interaction object. - case_dict (dict): The case dictionary. + interaction (discord.Interaction): The interaction object. + moderation (aurora.models.Moderation): The moderation object. resolved (bool, optional): Whether the case is resolved or not. Defaults to False. """ + target = await moderation.get_target() + moderator = await moderation.get_moderator() if resolved: - if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) - target_name = ( - f"`{target_user['name']}`" - if target_user["discriminator"] == "0" - else f"`{target_user['name']}#{target_user['discriminator']}`" - ) - elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) - if target_user["mention"]: - target_name = f"{target_user['mention']}" - else: - target_name = f"`{target_user['name']}`" - - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) - moderator_name = ( - f"`{moderator_user['name']}`" - if moderator_user["discriminator"] == "0" - else f"`{moderator_user['name']}#{moderator_user['discriminator']}`" - ) - embed = Embed( - title=f"📕 Case #{case_dict['moderation_id']:,} Resolved", + title=f"📕 Case #{moderation.id:,} Resolved", color=await interaction.client.get_embed_color(interaction.channel), ) - embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Timestamp:** | " + resolved_by = await moderation.get_resolved_by() + embed.description = f"**Type:** {str.title(moderation.moderation_type)}\n**Target:** {target.name} ({target.id})\n**Moderator:** {moderator.name} ({moderator.id})\n**Timestamp:** | " - if case_dict["duration"] != "NULL": - td = timedelta( - **{ - unit: int(val) - for unit, val in zip( - ["hours", "minutes", "seconds"], - case_dict["duration"].split(":"), - ) - } - ) + if moderation.duration is not None: duration_embed = ( - f"{humanize_timedelta(timedelta=td)} | " - if case_dict["expired"] == "0" - else str(humanize_timedelta(timedelta=td)) + f"{humanize_timedelta(timedelta=moderation.duration)} | " + if not moderation.expired + else str(humanize_timedelta(timedelta=moderation.duration)) ) embed.description = ( embed.description - + f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}" + + f"\n**Duration:** {duration_embed}\n**Expired:** {moderation.expired}" ) - embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False) + if moderation.metadata.items(): + for key, value in moderation.metadata.items(): + embed.description += f"\n**{key.title()}:** {value}" + + embed.add_field(name="Reason", value=box(moderation.reason), inline=False) - resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"]) - resolved_name = ( - resolved_user["name"] - if resolved_user["discriminator"] == "0" - else f"{resolved_user['name']}#{resolved_user['discriminator']}" - ) embed.add_field( name="Resolve Reason", - value=f"Resolved by `{resolved_name}` ({resolved_user['id']}) for:\n" - + box(case_dict["resolve_reason"]), + value=f"Resolved by `{resolved_by.name}` ({resolved_by.id}) for:\n" + + box(moderation.resolve_reason), inline=False, ) else: - if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) - target_name = ( - f"`{target_user['name']}`" - if target_user["discriminator"] == "0" - else f"`{target_user['name']}#{target_user['discriminator']}`" - ) - elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) - if target_user["mention"]: - target_name = target_user["mention"] - else: - target_name = f"`{target_user['name']}`" - - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) - moderator_name = ( - f"`{moderator_user['name']}`" - if moderator_user["discriminator"] == "0" - else f"`{moderator_user['name']}#{moderator_user['discriminator']}`" - ) - embed = Embed( - title=f"📕 Case #{case_dict['moderation_id']:,}", + title=f"📕 Case #{moderation.id:,}", color=await interaction.client.get_embed_color(interaction.channel), ) - embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Timestamp:** | " + embed.description = f"**Type:** {str.title(moderation.type)}\n**Target:** {target.name} ({target.id})\n**Moderator:** {moderator.name} ({moderator.id})\n**Timestamp:** | " - if case_dict["duration"] != "NULL": - td = timedelta( - **{ - unit: int(val) - for unit, val in zip( - ["hours", "minutes", "seconds"], - case_dict["duration"].split(":"), - ) - } - ) + if moderation.duration: embed.description = ( embed.description - + f"\n**Duration:** {humanize_timedelta(timedelta=td)} | " + + f"\n**Duration:** {humanize_timedelta(timedelta=moderation.duration)} | " ) - embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False) + if moderation.metadata.items(): + for key, value in moderation.metadata.items(): + embed.description += f"\n**{key.title()}:** {value}" + + embed.add_field(name="Reason", value=box(moderation.reason), inline=False) return embed -async def case_factory(interaction: Interaction, case_dict: dict) -> Embed: +async def case_factory(interaction: Interaction, moderation: Moderation) -> Embed: """This function creates a case embed from set parameters. Args: - interaction (Interaction): The interaction object. - case_dict (dict): The case dictionary. + interaction (discord.Interaction): The interaction object. + moderation (aurora.models.Moderation): The moderation object. """ - if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) - target_name = ( - f"`{target_user['name']}`" - if target_user["discriminator"] == "0" - else f"`{target_user['name']}#{target_user['discriminator']}`" - ) - elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) - if target_user["mention"]: - target_name = f"{target_user['mention']}" - else: - target_name = f"`{target_user['name']}`" - - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) - moderator_name = ( - f"`{moderator_user['name']}`" - if moderator_user["discriminator"] == "0" - else f"`{moderator_user['name']}#{moderator_user['discriminator']}`" - ) + target = await moderation.get_target() + moderator = await moderation.get_moderator() embed = Embed( - title=f"📕 Case #{case_dict['moderation_id']:,}", + title=f"📕 Case #{moderation.id:,}", color=await interaction.client.get_embed_color(interaction.channel), ) - embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Resolved:** {bool(case_dict['resolved'])}\n**Timestamp:** | " + embed.description = f"**Type:** {str.title(moderation.type)}\n**Target:** `{target.name}` ({target.id})\n**Moderator:** `{moderator.name}` ({moderator.id})\n**Resolved:** {moderation.resolved}\n**Timestamp:** | " - if case_dict["duration"] != "NULL": - td = timedelta( - **{ - unit: int(val) - for unit, val in zip( - ["hours", "minutes", "seconds"], case_dict["duration"].split(":") - ) - } - ) + if moderation.duration: duration_embed = ( - f"{humanize_timedelta(timedelta=td)} | " - if bool(case_dict["expired"]) is False - else str(humanize_timedelta(timedelta=td)) + f"{humanize_timedelta(timedelta=moderation.duration)} | " + if moderation.expired is False + else str(humanize_timedelta(timedelta=moderation.duration)) ) - embed.description += f"\n**Duration:** {duration_embed}\n**Expired:** {bool(case_dict['expired'])}" + embed.description += f"\n**Duration:** {duration_embed}\n**Expired:** {moderation.expired}" embed.description += ( - f"\n**Changes:** {len(case_dict['changes']) - 1}" - if case_dict["changes"] + f"\n**Changes:** {len(moderation.changes) - 1}" + if moderation.changes else "\n**Changes:** 0" ) - if case_dict["role_id"]: - embed.description += f"\n**Role:** <@&{case_dict['role_id']}>" + if moderation.role_id: + role = await moderation.get_role() + embed.description += f"\n**Role:** {role.name}" - if case_dict["metadata"]: - if case_dict["metadata"]["imported_from"]: + if moderation.metadata: + if moderation.metadata.get("imported_from"): embed.description += ( - f"\n**Imported From:** {case_dict['metadata']['imported_from']}" + f"\n**Imported From:** {moderation.metadata['imported_from']}" ) + moderation.metadata.pop("imported_from") + if moderation.metadata.get("imported_timestamp"): + embed.description += ( + f"\n**Imported Timestamp:** | " + ) + moderation.metadata.pop("imported_timestamp") + if moderation.metadata.items(): + for key, value in moderation.metadata.items(): + embed.description += f"\n**{key.title()}:** {value}" - embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False) + embed.add_field(name="Reason", value=box(moderation.reason), inline=False) - if case_dict["resolved"] == 1: - resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"]) - resolved_name = ( - f"`{resolved_user['name']}`" - if resolved_user["discriminator"] == "0" - else f"`{resolved_user['name']}#{resolved_user['discriminator']}`" - ) + if moderation.resolved: + resolved_user = await moderation.get_resolved_by() embed.add_field( name="Resolve Reason", - value=f"Resolved by {resolved_name} ({resolved_user['id']}) for:\n{box(case_dict['resolve_reason'])}", + value=f"Resolved by `{resolved_user.name}` ({resolved_user.id}) for:\n{box(moderation.resolve_reason)}", inline=False, ) return embed -async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed: +async def changes_factory(interaction: Interaction, moderation: Moderation) -> Embed: """This function creates a changes embed from set parameters. Args: - interaction (Interaction): The interaction object. - case_dict (dict): The case dictionary. + interaction (discord.Interaction): The interaction object. + moderation (aurora.models.Moderation): The moderation object. """ embed = Embed( - title=f"📕 Case #{case_dict['moderation_id']:,} Changes", + title=f"📕 Case #{moderation.id:,} Changes", color=await interaction.client.get_embed_color(interaction.channel), ) memory_dict = {} - if case_dict["changes"]: - for change in case_dict["changes"]: - if change["user_id"] not in memory_dict: - memory_dict[str(change["user_id"])] = await fetch_user_dict( - interaction.client, change["user_id"] - ) + if moderation.changes: + for change in moderation.changes: + if change.user_id not in memory_dict: + memory_dict[str(change.user_id)] = await change.get_user() - user = memory_dict[str(change["user_id"])] - name = ( - user["name"] - if user["discriminator"] == "0" - else f"{user['name']}#{user['discriminator']}" - ) + user: PartialUser = memory_dict[str(change.user_id)] - timestamp = f" | " + timestamp = f" | " - if change["type"] == "ORIGINAL": + if change.type == "ORIGINAL": embed.add_field( name="Original", - value=f"**User:** `{name}` ({user['id']})\n**Reason:** {change['reason']}\n**Timestamp:** {timestamp}", + value=f"**User:** `{user.name}` ({user.id})\n**Reason:** {change.reason}\n**Timestamp:** {timestamp}", inline=False, ) - elif change["type"] == "EDIT": + elif change.type == "EDIT": embed.add_field( name="Edit", - value=f"**User:** `{name}` ({user['id']})\n**Reason:** {change['reason']}\n**Timestamp:** {timestamp}", + value=f"**User:** `{user.name}` ({user.id})\n**Reason:** {change.reason}\n**Timestamp:** {timestamp}", inline=False, ) - elif change["type"] == "RESOLVE": + elif change.type == "RESOLVE": embed.add_field( name="Resolve", - value=f"**User:** `{name}` ({user['id']})\n**Reason:** {change['reason']}\n**Timestamp:** {timestamp}", + value=f"**User:** `{user.name}` ({user.id})\n**Reason:** {change.reason}\n**Timestamp:** {timestamp}", inline=False, ) @@ -353,44 +275,29 @@ async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed: return embed -async def evidenceformat_factory(interaction: Interaction, case_dict: dict) -> str: +async def evidenceformat_factory(moderation: Moderation) -> str: """This function creates a codeblock in evidence format from set parameters. Args: - interaction (Interaction): The interaction object. - case_dict (dict): The case dictionary. + interaction (discord.Interaction): The interaction object. + moderation (aurora.models.Moderation): The moderation object. """ - if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) - target_name = ( - target_user["name"] - if target_user["discriminator"] == "0" - else f"{target_user['name']}#{target_user['discriminator']}" - ) + target = await moderation.get_target() + moderator = await moderation.get_moderator() - elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) - target_name = target_user["name"] + content = f"Case: {moderation.id:,} ({str.title(moderation.type)})\nTarget: {target.name} ({target.id})\nModerator: {moderator.name} ({moderator.id})" - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) - moderator_name = ( - moderator_user["name"] - if moderator_user["discriminator"] == "0" - else f"{moderator_user['name']}#{moderator_user['discriminator']}" - ) + if moderation.duration is not None: + content += f"\nDuration: {humanize_timedelta(timedelta=moderation.duration)}" - content = f"Case: {case_dict['moderation_id']:,} ({str.title(case_dict['moderation_type'])})\nTarget: {target_name} ({target_user['id']})\nModerator: {moderator_name} ({moderator_user['id']})" + if moderation.role_id: + role = await moderation.get_role() + content += "\nRole: " + (role.name if role is not None else moderation.role_id) - if case_dict["role_id"] != "0": - role = interaction.guild.get_role(int(case_dict["role_id"])) - content += "\nRole: " + (role.name if role is not None else case_dict["role_id"]) + content += f"\nReason: {moderation.reason}" - if case_dict["duration"] != "NULL": - hours, minutes, seconds = map(int, case_dict["duration"].split(":")) - td = timedelta(hours=hours, minutes=minutes, seconds=seconds) - content += f"\nDuration: {humanize_timedelta(timedelta=td)}" - - content += f"\nReason: {case_dict['reason']}" + for key, value in moderation.metadata.items(): + content += f"\n{key.title()}: {value}" return box(content, "prolog") diff --git a/aurora/utilities/json.py b/aurora/utilities/json.py new file mode 100644 index 0000000..5dbd4eb --- /dev/null +++ b/aurora/utilities/json.py @@ -0,0 +1,131 @@ +import json +from datetime import datetime, timedelta +from typing import Any + +from redbot.core.bot import Red + +from ..models.base import AuroraBaseModel + + +class JSONEncoder(json.JSONEncoder): + def default(self, o) -> Any: + match o: + case datetime(): + return int(o.timestamp()) + case timedelta(): + return str(o) + case AuroraBaseModel(): + return o.dump() + case Red(): + return None + case _: + return super().default(o) + + +# This is a wrapper around the json module's dumps function that uses our custom JSONEncoder class +def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, indent=None, separators=None, + default=None, sort_keys=False, **kw) -> str: + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped + instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value can contain non-ASCII + characters if they appear in strings contained in ``obj``. Otherwise, all + such characters are escaped in JSON strings. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``RecursionError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If specified, ``separators`` should be an ``(item_separator, key_separator)`` + tuple. The default is ``(', ', ': ')`` if *indent* is ``None`` and + ``(',', ': ')`` otherwise. To get the most compact JSON representation, + you should specify ``(',', ':')`` to eliminate whitespace. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *sort_keys* is true (default: ``False``), then the output of + dictionaries will be sorted by key. + """ + return json.dumps( + obj, + cls=JSONEncoder, + skipkeys=skipkeys, + ensure_ascii=ensure_ascii, + check_circular=check_circular, + allow_nan=allow_nan, + indent=indent, + separators=separators, + default=default, + sort_keys=sort_keys, + **kw + ) + +# This is a wrapper around the json module's dump function that uses our custom JSONEncoder class +def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, indent=None, separators=None, + default=None, sort_keys=False, **kw) -> str: + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped + instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the strings written to ``fp`` can + contain non-ASCII characters if they appear in strings contained in + ``obj``. Otherwise, all such characters are escaped in JSON strings. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``RecursionError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If specified, ``separators`` should be an ``(item_separator, key_separator)`` + tuple. The default is ``(', ', ': ')`` if *indent* is ``None`` and + ``(',', ': ')`` otherwise. To get the most compact JSON representation, + you should specify ``(',', ':')`` to eliminate whitespace. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *sort_keys* is true (default: ``False``), then the output of + dictionaries will be sorted by key. + """ + return json.dump( + obj, + fp, + cls=JSONEncoder, + skipkeys=skipkeys, + ensure_ascii=ensure_ascii, + check_circular=check_circular, + allow_nan=allow_nan, + indent=indent, + separators=separators, + default=default, + sort_keys=sort_keys, + **kw + ) diff --git a/aurora/utilities/utils.py b/aurora/utilities/utils.py index 1ff21fa..ea39d65 100644 --- a/aurora/utilities/utils.py +++ b/aurora/utilities/utils.py @@ -1,23 +1,21 @@ # pylint: disable=cyclic-import -import json -from datetime import datetime -from datetime import timedelta as td -from typing import Optional, Union +from datetime import datetime, timedelta +from typing import Optional, Tuple, Union from dateutil.relativedelta import relativedelta as rd -from discord import File, Guild, Interaction, Member, SelectOption, User -from discord.errors import Forbidden, NotFound +from discord import File, Guild, Interaction, Member, SelectOption, TextChannel, User +from discord.errors import Forbidden from redbot.core import commands, data_manager from redbot.core.utils.chat_formatting import error -from .config import config +from ..utilities.config import config def check_permissions( user: User, - permissions: list, - ctx: Union[commands.Context, Interaction] = None, - guild: Guild = None, + permissions: Tuple[str], + ctx: Union[commands.Context, Interaction] | None = None, + guild: Guild | None = None, ) -> Union[bool, str]: """Checks if a user has a specific permission (or a list of permissions) in a channel.""" if ctx: @@ -42,9 +40,10 @@ def check_permissions( async def check_moddable( - target: Union[User, Member], interaction: Interaction, permissions: list + target: Union[User, Member, TextChannel], interaction: Interaction, permissions: Tuple[str] ) -> bool: """Checks if a moderator can moderate a target.""" + is_channel = isinstance(target, TextChannel) if check_permissions(interaction.client.user, permissions, guild=interaction.guild): await interaction.response.send_message( error( @@ -70,7 +69,7 @@ async def check_moddable( ) return False - if target.bot: + if not is_channel and target.bot: await interaction.response.send_message( content="You cannot moderate bots!", ephemeral=True ) @@ -111,122 +110,31 @@ async def check_moddable( return True -async def get_next_case_number(guild_id: str, cursor=None) -> int: - """This function returns the next case number from the MySQL table for a specific guild.""" - from .database import connect - - if not cursor: - database = connect() - cursor = database.cursor() - cursor.execute( - f"SELECT moderation_id FROM `moderation_{guild_id}` ORDER BY moderation_id DESC LIMIT 1" - ) - result = cursor.fetchone() - return (result[0] + 1) if result else 1 - - -def generate_dict(result) -> dict: - case = { - "moderation_id": result[0], - "timestamp": result[1], - "moderation_type": result[2], - "target_type": result[3], - "target_id": result[4], - "moderator_id": result[5], - "role_id": result[6], - "duration": result[7], - "end_timestamp": result[8], - "reason": result[9], - "resolved": result[10], - "resolved_by": result[11], - "resolve_reason": result[12], - "expired": result[13], - "changes": json.loads(result[14]), - "metadata": json.loads(result[15]), - } - return case - - -async def fetch_user_dict(client: commands.Bot, user_id: str) -> dict: - """This function returns a dictionary containing either user information or a standard deleted user template.""" - if user_id == "?": - user_dict = {"id": "?", "name": "Unknown User", "discriminator": "0"} - - else: - try: - user = client.get_user(int(user_id)) - if user is None: - user = await client.fetch_user(int(user_id)) - - user_dict = { - "id": user.id, - "name": user.name, - "discriminator": user.discriminator, - } - - except NotFound: - user_dict = { - "id": user_id, - "name": "Deleted User", - "discriminator": "0", - } - - - return user_dict - - -async def fetch_channel_dict(guild: Guild, channel_id: int) -> dict: - """This function returns a dictionary containing either channel information or a standard deleted channel template.""" - try: - channel = guild.get_channel(int(channel_id)) - if not channel: - channel = await guild.fetch_channel(channel_id) - - channel_dict = { - "id": channel.id, - "name": channel.name, - "mention": channel.mention, - } - - except NotFound: - channel_dict = {"id": channel_id, "name": "Deleted Channel", "mention": None} - - return channel_dict - - -async def fetch_role_dict(guild: Guild, role_id: int) -> dict: - """This function returns a dictionary containing either role information or a standard deleted role template.""" - role = guild.get_role(int(role_id)) - if not role: - role_dict = {"id": role_id, "name": "Deleted Role"} - - role_dict = {"id": role.id, "name": role.name} - - return role_dict - - async def log(interaction: Interaction, moderation_id: int, resolved: bool = False) -> None: """This function sends a message to the guild's configured logging channel when an infraction takes place.""" - from .database import fetch_case + from ..models.moderation import Moderation from .factory import log_factory logging_channel_id = await config.guild(interaction.guild).log_channel() if logging_channel_id != " ": logging_channel = interaction.guild.get_channel(logging_channel_id) - case = await fetch_case(moderation_id, interaction.guild.id) - if case: + try: + moderation = Moderation.find_by_id(interaction.client, moderation_id, interaction.guild_id) embed = await log_factory( - interaction=interaction, case_dict=case, resolved=resolved + interaction=interaction, moderation=moderation, resolved=resolved ) try: await logging_channel.send(embed=embed) except Forbidden: return + except ValueError: + return -async def send_evidenceformat(interaction: Interaction, case_dict: dict) -> None: +async def send_evidenceformat(interaction: Interaction, moderation_id: int) -> None: """This function sends an ephemeral message to the moderator who took the moderation action, with a pre-made codeblock for use in the mod-evidence channel.""" + from ..models.moderation import Moderation from .factory import evidenceformat_factory send_evidence_bool = ( @@ -237,26 +145,20 @@ async def send_evidenceformat(interaction: Interaction, case_dict: dict) -> None if send_evidence_bool is False: return - content = await evidenceformat_factory(interaction=interaction, case_dict=case_dict) + moderation = Moderation.find_by_id(interaction.client, moderation_id, interaction.guild.id) + content = await evidenceformat_factory(moderation=moderation) await interaction.followup.send(content=content, ephemeral=True) -def convert_timedelta_to_str(timedelta: td) -> str: - """This function converts a timedelta object to a string.""" - total_seconds = int(timedelta.total_seconds()) - hours = total_seconds // 3600 - minutes = (total_seconds % 3600) // 60 - seconds = total_seconds % 60 - return f"{hours}:{minutes}:{seconds}" - - def get_bool_emoji(value: Optional[bool]) -> str: """Returns a unicode emoji based on a boolean value.""" - if value is True: - return "\N{WHITE HEAVY CHECK MARK}" - if value is False: - return "\N{NO ENTRY SIGN}" - return "\N{BLACK QUESTION MARK ORNAMENT}\N{VARIATION SELECTOR-16}" + match value: + case True: + return "\N{WHITE HEAVY CHECK MARK}" + case False: + return "\N{NO ENTRY SIGN}" + case _: + return "\N{BLACK QUESTION MARK ORNAMENT}\N{VARIATION SELECTOR-16}" def get_pagesize_str(value: Union[int, None]) -> str: @@ -286,12 +188,24 @@ def create_pagesize_options() -> list[SelectOption]: ) return options -def timedelta_from_relativedelta(relativedelta: rd) -> td: +def timedelta_from_relativedelta(relativedelta: rd) -> timedelta: """Converts a relativedelta object to a timedelta object.""" now = datetime.now() then = now - relativedelta return now - then +def timedelta_from_string(string: str) -> timedelta: + """Converts a string to a timedelta object.""" + hours, minutes, seconds = map(int, string.split(":")) + return timedelta(hours=hours, minutes=minutes, seconds=seconds) + +def timedelta_to_string(td: timedelta) -> str: + """Converts a timedelta object to a string.""" + days = td.days * 24 + hours, remainder = divmod(td.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return f"{days + hours}:{minutes:02}:{seconds:02}" + def get_footer_image(coginstance: commands.Cog) -> File: """Returns the footer image for the embeds.""" image_path = data_manager.bundled_data_path(coginstance) / "arrow.png" diff --git a/poetry.lock b/poetry.lock index 017d640..96437ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -123,6 +123,17 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "apsw" version = "3.45.2.0" @@ -184,15 +195,29 @@ files = [ {file = "apsw-3.45.2.0.tar.gz", hash = "sha256:4fe81f5e390969d08d3048f357a68b347316b8f09455ff4657d94c56acfa255c"}, ] +[[package]] +name = "argcomplete" +version = "3.3.0" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +files = [ + {file = "argcomplete-3.3.0-py3-none-any.whl", hash = "sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54"}, + {file = "argcomplete-3.3.0.tar.gz", hash = "sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + [[package]] name = "astroid" -version = "3.1.0" +version = "3.2.2" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, - {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, + {file = "astroid-3.2.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, + {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, ] [[package]] @@ -343,13 +368,13 @@ files = [ [[package]] name = "cairocffi" -version = "1.6.1" +version = "1.7.0" description = "cffi-based cairo bindings for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cairocffi-1.6.1-py3-none-any.whl", hash = "sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e"}, - {file = "cairocffi-1.6.1.tar.gz", hash = "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7"}, + {file = "cairocffi-1.7.0-py3-none-any.whl", hash = "sha256:1f29a8d41dbda4090c0aa33bcdea64f3b493e95f74a43ea107c4a8a7b7f632ef"}, + {file = "cairocffi-1.7.0.tar.gz", hash = "sha256:7761863603894305f3160eca68452f373433ca8745ab7dd445bd2c6ce50dcab7"}, ] [package.dependencies] @@ -357,7 +382,7 @@ cffi = ">=1.1.0" [package.extras] doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "numpy", "pikepdf", "pytest"] +test = ["numpy", "pikepdf", "pytest", "ruff"] xcb = ["xcffib (>=1.4.0)"] [[package]] @@ -384,13 +409,13 @@ test = ["flake8", "isort", "pytest"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -801,30 +826,31 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.42" +version = "3.1.43" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, - {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] +doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] name = "griffe" -version = "0.42.1" +version = "0.45.2" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.42.1-py3-none-any.whl", hash = "sha256:7e805e35617601355edcac0d3511cedc1ed0cb1f7645e2d336ae4b05bbae7b3b"}, - {file = "griffe-0.42.1.tar.gz", hash = "sha256:57046131384043ed078692b85d86b76568a686266cc036b9b56b704466f803ce"}, + {file = "griffe-0.45.2-py3-none-any.whl", hash = "sha256:297ec8530d0c68e5b98ff86fb588ebc3aa3559bb5dc21f3caea8d9542a350133"}, + {file = "griffe-0.45.2.tar.gz", hash = "sha256:83ce7dcaafd8cb7f43cbf1a455155015a1eb624b1ffd93249e5e1c4a22b2fdb2"}, ] [package.dependencies] @@ -857,13 +883,13 @@ colors = ["colorama (>=0.4.6)"] [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1107,13 +1133,13 @@ pytz = "*" [[package]] name = "mkdocs-material" -version = "9.5.15" +version = "9.5.18" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.15-py3-none-any.whl", hash = "sha256:e5c96dec3d19491de49ca643fc1dbb92b278e43cdb816c775bc47db77d9b62fb"}, - {file = "mkdocs_material-9.5.15.tar.gz", hash = "sha256:39f03cca45e82bf54eb7456b5a18bd252eabfdd67f237a229471484a0a4d4635"}, + {file = "mkdocs_material-9.5.18-py3-none-any.whl", hash = "sha256:1e0e27fc9fe239f9064318acf548771a4629d5fd5dfd45444fd80a953fe21eb4"}, + {file = "mkdocs_material-9.5.18.tar.gz", hash = "sha256:a43f470947053fa2405c33995f282d24992c752a50114f23f30da9d8d0c57e62"}, ] [package.dependencies] @@ -1529,6 +1555,24 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa typing = ["typing-extensions"] xmp = ["defusedxml"] +[[package]] +name = "pipx" +version = "1.6.0" +description = "Install and Run Python Applications in Isolated Environments" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pipx-1.6.0-py3-none-any.whl", hash = "sha256:760889dc3aeed7bf4024973bf22ca0c2a891003f52389159ab5cb0c57d9ebff4"}, + {file = "pipx-1.6.0.tar.gz", hash = "sha256:840610e00103e3d49ae24b6b51804b60988851a5dd65468adb71e5a97e2699b2"}, +] + +[package.dependencies] +argcomplete = ">=1.9.4" +colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""} +packaging = ">=20" +platformdirs = ">=2.1" +userpath = ">=1.6,<1.9 || >1.9" + [[package]] name = "platformdirs" version = "4.2.0" @@ -1588,15 +1632,125 @@ requests = ">=2.21.0" [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.7.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, + {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.4" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"}, + {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"}, + {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"}, + {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"}, + {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"}, + {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"}, + {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"}, + {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"}, + {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"}, + {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"}, + {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"}, + {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"}, + {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"}, + {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.17.2" @@ -1614,17 +1768,17 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.1.0" +version = "3.2.2" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, - {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, + {file = "pylint-3.2.2-py3-none-any.whl", hash = "sha256:3f8788ab20bb8383e06dd2233e50f8e08949cfd9574804564803441a4946eab4"}, + {file = "pylint-3.2.2.tar.gz", hash = "sha256:d068ca1dfd735fb92a07d33cb8f288adc0f6bc1287a139ca2425366f7cbe38f8"}, ] [package.dependencies] -astroid = ">=3.1.0,<=3.2.0-dev0" +astroid = ">=3.2.2,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""} isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" @@ -1638,17 +1792,17 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.7.1" +version = "10.8.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, - {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, ] [package.dependencies] -markdown = ">=3.5" +markdown = ">=3.6" pyyaml = "*" [package.extras] @@ -1946,115 +2100,101 @@ test = ["pytest (>=7)", "pytest-asyncio (>=0.19)"] [[package]] name = "regex" -version = "2023.12.25" +version = "2024.5.15" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, - {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, - {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, - {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, - {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, - {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, - {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, - {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, - {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, - {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, - {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, - {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, - {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, - {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, - {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, + {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, + {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, + {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, + {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, + {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, + {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, + {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, + {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, + {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, + {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, + {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, ] [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -2087,28 +2227,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.3.4" +version = "0.3.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, - {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, - {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, - {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, - {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, - {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, + {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, + {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, + {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, + {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] [[package]] @@ -2160,13 +2300,13 @@ files = [ [[package]] name = "tinycss2" -version = "1.2.1" +version = "1.3.0" description = "A tiny CSS parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, + {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, + {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, ] [package.dependencies] @@ -2174,17 +2314,17 @@ webencodings = ">=0.4" [package.extras] doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "pytest"] +test = ["pytest", "ruff"] [[package]] name = "tomlkit" -version = "0.12.4" +version = "0.12.5" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, ] [[package]] @@ -2215,6 +2355,20 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "userpath" +version = "1.9.2" +description = "Cross-platform tool for adding locations to the user PATH" +optional = false +python-versions = ">=3.7" +files = [ + {file = "userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d"}, + {file = "userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815"}, +] + +[package.dependencies] +click = "*" + [[package]] name = "uvloop" version = "0.19.0" @@ -2261,40 +2415,43 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "watchdog" -version = "4.0.0" +version = "4.0.1" description = "Filesystem events monitoring" optional = false python-versions = ">=3.8" files = [ - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, - {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, - {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, - {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, - {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, - {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, - {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, - {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, ] [package.extras] @@ -2498,4 +2655,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "229d7fd39618cf708f3cd5409dde2e6e25b822e4f936e14b3ade9800bf00daab" +content-hash = "3f732c0b0b0eb2a31fb9484c7cf699cd3c26474d6779528102af4c509a48351e" diff --git a/pyproject.toml b/pyproject.toml index 872ccdf..a2f2034 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,12 @@ package-mode = false [tool.poetry.dependencies] python = ">=3.11,<3.12" -Red-DiscordBot = "^3.5.5" +Red-DiscordBot = "^3.5.9" py-dactyl = "^2.0.4" websockets = "^12.0" pillow = "^10.3.0" numpy = "^1.26.4" +pydantic = "^2.7.1" colorthief = "^0.2.1" beautifulsoup4 = "^4.12.3" markdownify = "^0.12.1" @@ -24,6 +25,7 @@ optional = true [tool.poetry.group.dev.dependencies] ruff = "^0.3.1" pylint = "^3.1.0" +pipx = "^1.5.0" [tool.poetry.group.docs] optional = true