diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c08c396..50a99c7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,35 +1,38 @@ -FROM ghcr.io/astral-sh/uv:0.5.24@sha256:2381d6aa60c326b71fd40023f921a0a3b8f91b14d5db6b90402e65a635053709 AS uv +FROM ghcr.io/astral-sh/uv:0.5.30@sha256:bb74263127d6451222fe7f71b330edfb189ab1c98d7898df2401fbf4f272d9b9 AS uv FROM python:3.11-slim@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS python +FROM code.forgejo.org/forgejo/runner:6.2.1@sha256:fecc96a111a15811a6887ce488e75718089f24599e613e93db8e54fe70b706e8 AS forgejo-runner FROM mcr.microsoft.com/vscode/devcontainers/base:bookworm@sha256:6155a486f236fd5127b76af33086029d64f64cf49dd504accb6e5f949098eb7e LABEL repository="www.coastalcommits.com/cswimr/SeaCogs" LABEL maintainer="cswimr " RUN apt-get update; \ - apt-get install -y --no-install-recommends \ - # Red-DiscordBot - build-essential \ - git \ - # PyNaCl - libsodium-dev \ - # CFFI - libffi-dev \ - # SSH repository support - openssh-client \ - # Cog dependencies - # Audio - openjdk-17-jre-headless \ - # PyLav - libaio1 \ - libaio-dev \ - # SeaUtils - dnsutils; \ - apt-get clean; \ - rm -rf /var/lib/apt/lists/* + apt-get install -y --no-install-recommends \ + # Red-DiscordBot + build-essential \ + git \ + # PyNaCl + libsodium-dev \ + # CFFI + libffi-dev \ + # SSH repository support + openssh-client \ + # Cog dependencies + # Audio + openjdk-17-jre-headless \ + # PyLav + libaio1 \ + libaio-dev \ + # SeaUtils + dnsutils; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* COPY --from=uv --chown=vscode: /uv /uvx /bin/ COPY --from=python --chown=vscode: /usr/local /usr/local +COPY --from=forgejo-runner --chown=vscode: /bin/forgejo-runner /bin/forgejo-runner +COPY --chown=vscode: .devcontainer/home/* /home/vscode/ RUN ln -s /usr/local/bin/python3.11 /usr/local/bin/python; \ - python --version; \ - python -m ensurepip + python --version; \ + python -m ensurepip diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 06e81b7..9bf34c9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,8 +4,21 @@ "context": "..", "dockerfile": "Dockerfile" }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, "customizations": { "vscode": { + "settings": { + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/bin/zsh" + } + } + }, "extensions": [ "charliermarsh.ruff", "ms-azuretools.vscode-docker", @@ -29,7 +42,11 @@ "UV_PYTHON_DOWNLOADS": "never", "PROJECT_DIR": "/workspaces/SeaCogs" }, - "mounts": ["source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume"], - "postCreateCommand": "uv sync --frozen && sudo chown -R vscode:vscode /workspaces/SeaCogs/.data && uv run redbot-setup --no-prompt --instance-name=local --data-path=/workspaces/SeaCogs/.data --backend=json", + "mounts": [ + "source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume" + ], + "postCreateCommand": { + "Setup Virtual Environment": "uv sync --frozen && sudo chown -R vscode:vscode /workspaces/SeaCogs/.data && uv run redbot-setup --no-prompt --instance-name=local --data-path=/workspaces/SeaCogs/.data --backend=json" + }, "remoteUser": "vscode" } diff --git a/.devcontainer/home/.bash_aliases b/.devcontainer/home/.bash_aliases new file mode 100644 index 0000000..277fc5f --- /dev/null +++ b/.devcontainer/home/.bash_aliases @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +alias runactions="forgejo-runner exec --default-actions-url=https://www.coastalcommits.com --gitea-instance=https://www.coastalcommits.com" diff --git a/.docs/aurora/index.md b/.docs/aurora/index.md index 4c6f321..0191f85 100644 --- a/.docs/aurora/index.md +++ b/.docs/aurora/index.md @@ -12,5 +12,5 @@ Aurora is a fully-featured moderation system. It is heavily inspired by Galactic ```bash [p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs [p]cog install seacogs aurora -[p]cog load aurora +[p]load aurora ``` diff --git a/.docs/backup.md b/.docs/backup.md index 4ddc56f..26c949d 100644 --- a/.docs/backup.md +++ b/.docs/backup.md @@ -7,7 +7,7 @@ Backup allows you to export a JSON list of all of your installed repositories an ```bash [p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs [p]cog install seacogs backup -[p]cog load backup +[p]load backup ``` ## Version Compatibility diff --git a/.docs/bible.md b/.docs/bible.md index 188f985..0db826b 100644 --- a/.docs/bible.md +++ b/.docs/bible.md @@ -8,7 +8,7 @@ This cog does require an api key to work. ```bash [p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs [p]cog install seacogs bible -[p]cog load bible +[p]load bible ``` ## Setup @@ -21,8 +21,9 @@ Then, you can use `[p]set api` to set the API key. Make sure your formatting mat ## Commands ### bible passage - - Usage: `[p]bible passage ` - - Aliases: `verse` + +- Usage: `[p]bible passage ` +- Aliases: `verse` Get a Bible passage. @@ -31,6 +32,7 @@ Example usage: `[p]bible passage John 3:16-3:17` ### bible random - - Usage: `[p]bible random` + +- Usage: `[p]bible random` Get a random Bible verse. diff --git a/.docs/emojiinfo.md b/.docs/emojiinfo.md index ef63ab6..fc010a5 100644 --- a/.docs/emojiinfo.md +++ b/.docs/emojiinfo.md @@ -7,7 +7,7 @@ EmojiInfo allows you to retrieve information about an emoji. ```bash [p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs [p]cog install seacogs emojiinfo -[p]cog load emojiinfo +[p]load emojiinfo ``` ## Commands diff --git a/.docs/hotreload.md b/.docs/hotreload.md new file mode 100644 index 0000000..0144e5d --- /dev/null +++ b/.docs/hotreload.md @@ -0,0 +1,26 @@ +# HotReload + +HotReload automatically reloads cogs in local cog paths on file change. +This is useful for development, as it allows you to make changes to your cogs and see the changes reflected in Discord immediately, without having to manually `[p]reload` the cog. + +## Installation + +```bash +[p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs +[p]cog install seacogs hotreload +[p]load hotreload +``` + +## Commands + +### hotreload compile + +Determines if the cog should try to compile a modified Python file before reloading the associated cog. Useful for catching syntax errors. Disabled by default. + +### hotreload notifychannel + +Set the channel where hotreload will send notifications when a cog is reloaded. + +### hotreload list + +Debugging command that shows the list of currently active observers. May be expanded in the future to show watched file paths. diff --git a/.docs/nerdify.md b/.docs/nerdify.md index 87662b6..ceef1af 100644 --- a/.docs/nerdify.md +++ b/.docs/nerdify.md @@ -7,7 +7,7 @@ Nerdify allows you to nerdify other people's text. ```bash [p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs [p]cog install seacogs nerdify -[p]cog load nerdify +[p]load nerdify ``` ## Commands diff --git a/.docs/pterodactyl/index.md b/.docs/pterodactyl/index.md index cb69a5d..732b505 100644 --- a/.docs/pterodactyl/index.md +++ b/.docs/pterodactyl/index.md @@ -12,5 +12,5 @@ Pterodactyl allows for connecting to a Pterodactyl server through websockets. It ```bash [p]repo add seacogs https://www.coastalcommits.com/cswimr/SeaCogs [p]cog install seacogs pterodactyl -[p]cog load aurora +[p]load pterodactyl ``` diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md index efe146e..33f957c 100644 --- a/.forgejo/pull_request_template.md +++ b/.forgejo/pull_request_template.md @@ -2,5 +2,5 @@ -- [ ] By submitting this pull request, I permit cswimr to license my work under +- [ ] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com/cswimr) to license my work under the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE). diff --git a/.gitignore b/.gitignore index d937fba..3902499 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ site .venv .data __pycache__ +.mypy_cache/ +.ruff_cache/ +.direnv/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 9a7e46e..2a3559f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,15 +1,12 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Red-DiscordBot", - "type": "debugpy", - "request": "launch", - "module": "redbot", - "args": ["local", "--dev", "-vvv", "--load-cogs=hotreload"] - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Red-DiscordBot", + "type": "debugpy", + "request": "launch", + "module": "redbot", + "args": ["local", "--dev", "-vvv", "--load-cogs=hotreload"] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6808e51..68a52a5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { "[python]": { - "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, @@ -8,5 +7,26 @@ }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" - } + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "files.exclude": { + "**/.git": true, + "**/__pycache__": true, + "**/.ruff_cache": true, + "**/.mypy_cache": true + }, + "python.analysis.diagnosticSeverityOverrides": { + "reportAttributeAccessIssue": false, // disabled because `commands.group.command` is listed as Any / Unknown for some reason + "reportCallIssue": "information" + }, + "python.analysis.diagnosticMode": "workspace", + "python.analysis.supportDocstringTemplate": true, + "python.analysis.typeCheckingMode": "basic", + "python.analysis.typeEvaluation.enableReachabilityAnalysis": true, + "python.analysis.typeEvaluation.strictDictionaryInference": true, + "python.analysis.typeEvaluation.strictListInference": true, + "python.analysis.typeEvaluation.strictSetInference": true, + "editor.formatOnSave": true, } diff --git a/antipolls/antipolls.py b/antipolls/antipolls.py index 15aecfd..6f6dac6 100644 --- a/antipolls/antipolls.py +++ b/antipolls/antipolls.py @@ -17,7 +17,7 @@ class AntiPolls(commands.Cog): __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "1.0.1" + __version__ = "1.0.3" __documentation__ = "https://seacogs.coastalcommits.com/antipolls/" def __init__(self, bot: Red): @@ -45,11 +45,11 @@ class AntiPolls(commands.Cog): ] return "\n".join(text) - async def red_delete_data_for_user(self, **kwargs): # pylint: disable=unused-argument + async def red_delete_data_for_user(self, **kwargs): # pylint: disable=unused-argument """Nothing to delete.""" return - @commands.Cog.listener('on_message') + @commands.Cog.listener("on_message") async def polls_listener(self, message: discord.Message) -> None: if message.guild is None: return self.logger.verbose("Message in direct messages ignored") @@ -62,13 +62,13 @@ class AntiPolls(commands.Cog): guild_config = await self.config.guild(message.guild).all() - if guild_config['manage_messages'] is True and message.author.guild_permissions.manage_messages: + if guild_config["manage_messages"] is True and message.author.guild_permissions.manage_messages: return self.logger.verbose("Message from user with Manage Messages permission ignored") - if message.channel.id in guild_config['channel_whitelist']: + if message.channel.id in guild_config["channel_whitelist"]: return self.logger.verbose("Message in whitelisted channel %s ignored", message.channel.id) - if any(role.id in guild_config['role_whitelist'] for role in message.author.roles): + if any(role.id in guild_config["role_whitelist"] for role in message.author.roles): return self.logger.verbose("Message from whitelisted role %s ignored", message.author.roles) if not message.content and not message.embeds and not message.attachments and not message.stickers: @@ -80,9 +80,9 @@ class AntiPolls(commands.Cog): return self.logger.error("Failed to delete message: %s", e) return self.logger.trace("Deleted poll message %s", message.id) - self.logger.verbose("Message %s is not a poll, ignoring", message.id) + return self.logger.verbose("Message %s is not a poll, ignoring", message.id) - @commands.group(name="antipolls", aliases=["ap"]) + @commands.group(name="antipolls", aliases=["ap"]) # type: ignore @commands.guild_only() @commands.admin_or_permissions(manage_guild=True) async def antipolls(self, ctx: commands.Context) -> None: @@ -95,6 +95,8 @@ class AntiPolls(commands.Cog): @antipolls_roles.command(name="add") async def antipolls_roles_add(self, ctx: commands.Context, *roles: discord.Role) -> None: """Add roles to the whitelist.""" + assert ctx.guild is not None # using `assert` here and in the rest of this file to satisfy typecheckers + # this is safe because the commands are part of a guild-only command group async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist: role_whitelist: list failed: list[discord.Role] = [] @@ -110,6 +112,7 @@ class AntiPolls(commands.Cog): @antipolls_roles.command(name="remove") async def antipolls_roles_remove(self, ctx: commands.Context, *roles: discord.Role) -> None: """Remove roles from the whitelist.""" + assert ctx.guild is not None async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist: role_whitelist: list failed: list[discord.Role] = [] @@ -123,13 +126,14 @@ class AntiPolls(commands.Cog): await ctx.send(f"The following roles were not in the whitelist: {humanize_list([role.mention for role in failed])}", delete_after=10) @antipolls_roles.command(name="list") - async def antipolls_roles_list(self, ctx: commands.Context) -> None: + async def antipolls_roles_list(self, ctx: commands.Context) -> discord.Message: """List roles in the whitelist.""" + assert ctx.guild is not None role_whitelist = await self.config.guild(ctx.guild).role_whitelist() if not role_whitelist: return await ctx.send("No roles in the whitelist.") - roles = [ctx.guild.get_role(role) for role in role_whitelist] - await ctx.send(humanize_list([role.mention for role in roles])) + roles = [role for role in (ctx.guild.get_role(role) for role in role_whitelist) if role is not None] + return await ctx.send(humanize_list([role.mention for role in roles])) @antipolls.group(name="channels") async def antipolls_channels(self, ctx: commands.Context) -> None: @@ -138,6 +142,7 @@ class AntiPolls(commands.Cog): @antipolls_channels.command(name="add") async def antipolls_channels_add(self, ctx: commands.Context, *channels: discord.TextChannel) -> None: """Add channels to the whitelist.""" + assert ctx.guild is not None async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist: channel_whitelist: list failed: list[discord.TextChannel] = [] @@ -153,6 +158,7 @@ class AntiPolls(commands.Cog): @antipolls_channels.command(name="remove") async def antipolls_channels_remove(self, ctx: commands.Context, *channels: discord.TextChannel) -> None: """Remove channels from the whitelist.""" + assert ctx.guild is not None async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist: channel_whitelist: list failed: list[discord.TextChannel] = [] @@ -166,16 +172,21 @@ class AntiPolls(commands.Cog): await ctx.send(f"The following channels were not in the whitelist: {humanize_list([channel.mention for channel in failed])}", delete_after=10) @antipolls_channels.command(name="list") - async def antipolls_channels_list(self, ctx: commands.Context) -> None: + async def antipolls_channels_list(self, ctx: commands.Context) -> discord.Message: """List channels in the whitelist.""" + assert ctx.guild is not None channel_whitelist = await self.config.guild(ctx.guild).channel_whitelist() if not channel_whitelist: return await ctx.send("No channels in the whitelist.") - channels = [ctx.guild.get_channel(channel) for channel in channel_whitelist] - await ctx.send(humanize_list([channel.mention for channel in channels])) + channels = [channel for channel in (ctx.guild.get_channel(channel) for channel in channel_whitelist) if channel is not None] + for c in channels: + if not c: + channels.remove(c) + return await ctx.send(humanize_list([channel.mention for channel in channels])) @antipolls.command(name="managemessages") async def antipolls_managemessages(self, ctx: commands.Context, enabled: bool) -> None: """Toggle Manage Messages permission check.""" + assert ctx.guild is not None await self.config.guild(ctx.guild).manage_messages.set(enabled) await ctx.tick() diff --git a/antipolls/info.json b/antipolls/info.json index 7856195..24bc0bb 100644 --- a/antipolls/info.json +++ b/antipolls/info.json @@ -1,17 +1,14 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", - "name" : "AntiPolls", - "short" : "AntiPolls deletes messages that contain polls.", - "description" : "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).", - "end_user_data_statement" : "This cog does not store any user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", + "name": "AntiPolls", + "short": "AntiPolls deletes messages that contain polls.", + "description": "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).", + "end_user_data_statement": "This cog does not store any user data.", "hidden": true, "disabled": false, "min_bot_version": "3.5.0", "min_python_version": [3, 10, 0], - "tags": [ - "automod", - "automoderation", - "polls" - ] + "tags": ["automod", "automoderation", "polls"] } diff --git a/backup/backup.py b/backup/backup.py index dfc5397..803e863 100644 --- a/backup/backup.py +++ b/backup/backup.py @@ -17,13 +17,16 @@ from redbot.core.bot import Red from redbot.core.utils.chat_formatting import bold, error, humanize_list, text_to_file +# Disable Ruff & Pylint complaining about accessing private members +# That's kind of necessary for this cog to function because the Downloader cog has a limited public API +# ruff: noqa: SLF001 # Private member access # pylint: disable=protected-access class Backup(commands.Cog): """A utility to make reinstalling repositories and cogs after migrating the bot far easier.""" __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "1.1.1" + __version__ = "1.1.3" __documentation__ = "https://seacogs.coastalcommits.com/backup/" def __init__(self, bot: Red): @@ -42,22 +45,19 @@ class Backup(commands.Cog): ] return "\n".join(text) - @commands.group(autohelp=True) + @commands.group(autohelp=True) # type: ignore @commands.is_owner() - async def backup(self, ctx: commands.Context): + async def backup(self, ctx: commands.Context) -> None: """Backup your installed cogs.""" + pass @backup.command(name="export") @commands.is_owner() - async def backup_export(self, ctx: commands.Context): + async def backup_export(self, ctx: commands.Context) -> None: """Export your installed repositories and cogs to a file.""" downloader = ctx.bot.get_cog("Downloader") if downloader is None: - await ctx.send( - error( - f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again." - ) - ) + await ctx.send(error(f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again.")) return all_repos = list(downloader._repo_manager.repos) @@ -78,7 +78,7 @@ class Backup(commands.Cog): if cog.repo_name == repo.name: cog_dict = { "name": cog.name, - # "loaded": cog.name in ctx.bot.extensions.keys(), + # "loaded": cog.name in ctx.bot.extensions.keys(), # noqa: ERA001 # this functionality was planned but never implemented due to Red limitations # and the possibility of restoration functionality being added to Core "pinned": cog.pinned, @@ -88,30 +88,24 @@ class Backup(commands.Cog): export_data.append(repo_dict) - await ctx.send( - file=text_to_file(json.dumps(export_data, indent=4), "backup.json") - ) + await ctx.send(file=text_to_file(json.dumps(export_data, indent=4), "backup.json")) @backup.command(name="import") @commands.is_owner() - async def backup_import(self, ctx: commands.Context): + async def backup_import(self, ctx: commands.Context) -> None: """Import your installed repositories and cogs from an export file.""" try: export = json.loads(await ctx.message.attachments[0].read()) except (json.JSONDecodeError, IndexError): try: - export = json.loads(await ctx.message.reference.resolved.attachments[0].read()) + export = json.loads(await ctx.message.reference.resolved.attachments[0].read()) # type: ignore - this is fine to let error because it gets handled except (json.JSONDecodeError, IndexError, AttributeError): await ctx.send(error("Please provide a valid JSON export file.")) return downloader = ctx.bot.get_cog("Downloader") if downloader is None: - await ctx.send( - error( - f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again." - ) - ) + await ctx.send(error(f"You do not have the `Downloader` cog loaded. Please run `{ctx.prefix}load downloader` and try again.")) return repo_s = [] @@ -133,32 +127,20 @@ class Backup(commands.Cog): repo_e.append("PyLav cogs are not supported.") continue if name.startswith(".") or name.endswith("."): - repo_e.append( - f"Invalid repository name: {name}\nRepository names cannot start or end with a dot." - ) + repo_e.append(f"Invalid repository name: {name}\nRepository names cannot start or end with a dot.") continue if re.match(r"^[a-zA-Z0-9_\-\.]+$", name) is None: - repo_e.append( - f"Invalid repository name: {name}\nRepository names may only contain letters, numbers, underscores, hyphens, and dots." - ) + repo_e.append(f"Invalid repository name: {name}\nRepository names may only contain letters, numbers, underscores, hyphens, and dots.") continue try: - repository = await downloader._repo_manager.add_repo( - url, name, branch - ) - repo_s.append( - f"Added repository {name} from {url} on branch {branch}." - ) - self.logger.debug( - "Added repository %s from %s on branch %s", name, url, branch - ) + repository = await downloader._repo_manager.add_repo(url, name, branch) + repo_s.append(f"Added repository {name} from {url} on branch {branch}.") + self.logger.debug("Added repository %s from %s on branch %s", name, url, branch) except errors.ExistingGitRepo: repo_e.append(f"Repository {name} already exists.") - repository = downloader._repo_manager.get_repo( - name - ) + repository = downloader._repo_manager.get_repo(name) self.logger.debug("Repository %s already exists", name) except errors.AuthenticationError as err: @@ -172,9 +154,7 @@ class Backup(commands.Cog): continue except errors.CloningError as err: - repo_e.append( - f"Cloning error while adding repository {name}. See logs for more information." - ) + repo_e.append(f"Cloning error while adding repository {name}. See logs for more information.") self.logger.exception( "Something went wrong whilst cloning %s (to revision %s)", url, @@ -184,9 +164,7 @@ class Backup(commands.Cog): continue except OSError: - repo_e.append( - f"OS error while adding repository {name}. See logs for more information." - ) + repo_e.append(f"OS error while adding repository {name}. See logs for more information.") self.logger.exception( "Something went wrong trying to add repo %s under name %s", url, @@ -206,23 +184,19 @@ class Backup(commands.Cog): continue cog_modules.append(cog_module) - for cog in set(cog.name for cog in cog_modules): + for cog in {cog.name for cog in cog_modules}: poss_installed_path = (await downloader.cog_install_path()) / cog if poss_installed_path.exists(): with contextlib.suppress(commands.ExtensionNotLoaded): await ctx.bot.unload_extension(cog) await ctx.bot.remove_loaded_package(cog) - await downloader._delete_cog( - poss_installed_path - ) + await downloader._delete_cog(poss_installed_path) uninstall_s.append(f"Uninstalled {cog}") self.logger.debug("Uninstalled %s", cog) else: uninstall_e.append(f"Failed to uninstall {cog}") self.logger.warning("Failed to uninstall %s", cog) - await downloader._remove_from_installed( - cog_modules - ) + await downloader._remove_from_installed(cog_modules) for cog in cogs: cog_name = cog["name"] @@ -236,25 +210,15 @@ class Backup(commands.Cog): if cog_name == "backup" and "cswimr/SeaCogs" in url: continue - async with repository.checkout( - commit, exit_to_rev=repository.branch - ): - cogs_c, message = ( - await downloader._filter_incorrect_cogs_by_names( - repository, [cog_name] - ) - ) + async with repository.checkout(commit, exit_to_rev=repository.branch): + cogs_c, message = await downloader._filter_incorrect_cogs_by_names(repository, [cog_name]) if not cogs_c: install_e.append(message) self.logger.error(message) continue - failed_reqs = await downloader._install_requirements( - cogs_c - ) + failed_reqs = await downloader._install_requirements(cogs_c) if failed_reqs: - install_e.append( - f"Failed to install {cog_name} due to missing requirements: {failed_reqs}" - ) + install_e.append(f"Failed to install {cog_name} due to missing requirements: {failed_reqs}") self.logger.error( "Failed to install %s due to missing requirements: %s", cog_name, @@ -262,51 +226,37 @@ class Backup(commands.Cog): ) continue - installed_cogs, failed_cogs = await downloader._install_cogs( - cogs_c - ) + installed_cogs, failed_cogs = await downloader._install_cogs(cogs_c) if repository.available_libraries: - installed_libs, failed_libs = ( - await repository.install_libraries( - target_dir=downloader.SHAREDLIB_PATH, - req_target_dir=downloader.LIB_PATH, - ) + installed_libs, failed_libs = await repository.install_libraries( + target_dir=downloader.SHAREDLIB_PATH, + req_target_dir=downloader.LIB_PATH, ) else: installed_libs = None failed_libs = None if cog_pinned: - for cog in installed_cogs: + for cog in installed_cogs: # noqa: PLW2901 cog.pinned = True - await downloader._save_to_installed( - installed_cogs + installed_libs - if installed_libs - else installed_cogs - ) + await downloader._save_to_installed(installed_cogs + installed_libs if installed_libs else installed_cogs) if installed_cogs: installed_cog_name = installed_cogs[0].name install_s.append(f"Installed {installed_cog_name}") self.logger.debug("Installed %s", installed_cog_name) if installed_libs: for lib in installed_libs: - install_s.append( - f"Installed {lib.name} required for {cog_name}" - ) - self.logger.debug( - "Installed %s required for %s", lib.name, cog_name - ) + install_s.append(f"Installed {lib.name} required for {cog_name}") + self.logger.debug("Installed %s required for %s", lib.name, cog_name) if failed_cogs: failed_cog_name = failed_cogs[0].name install_e.append(f"Failed to install {failed_cog_name}") self.logger.error("Failed to install %s", failed_cog_name) if failed_libs: for lib in failed_libs: - install_e.append( - f"Failed to install {lib.name} required for {cog_name}" - ) + install_e.append(f"Failed to install {lib.name} required for {cog_name}") self.logger.error( "Failed to install %s required for %s", lib.name, diff --git a/backup/info.json b/backup/info.json index 2e465b0..c9a9269 100644 --- a/backup/info.json +++ b/backup/info.json @@ -1,15 +1,22 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing Backup!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", - "name" : "Backup", - "short" : "A utility to make reinstalling repositories and cogs after migrating the bot far easier.", - "description" : "A utility to make reinstalling repositories and cogs after migrating the bot far easier.", - "end_user_data_statement" : "This cog does not store end user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": [ + "cswimr" + ], + "install_msg": "Thank you for installing Backup!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", + "name": "Backup", + "short": "A utility to make reinstalling repositories and cogs after migrating the bot far easier.", + "description": "A utility to make reinstalling repositories and cogs after migrating the bot far easier.", + "end_user_data_statement": "This cog does not store end user data.", "hidden": false, "disabled": false, "min_bot_version": "3.5.6", - "max_bot_version": "3.5.13", - "min_python_version": [3, 9, 0], + "max_bot_version": "3.5.16", + "min_python_version": [ + 3, + 9, + 0 + ], "tags": [ "utility", "backup", diff --git a/bible/bible.py b/bible/bible.py index 9cf520b..cd95566 100644 --- a/bible/bible.py +++ b/bible/bible.py @@ -27,7 +27,7 @@ class Bible(commands.Cog): __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "1.1.2" + __version__ = "1.1.4" __documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/" def __init__(self, bot: Red): @@ -91,20 +91,20 @@ class Bible(commands.Cog): response.status, ) if response.status == 401: - raise bible.errors.Unauthorized() + raise bible.errors.UnauthorizedError if response.status == 403: - raise bible.errors.BibleAccessError() + raise bible.errors.BibleAccessError if response.status == 503: - raise bible.errors.ServiceUnavailable() + raise bible.errors.ServiceUnavailableError return Version( - bible_id, - data["data"]["abbreviation"], - data["data"]["language"]["name"], - data["data"]["abbreviationLocal"], - data["data"]["language"]["nameLocal"], - data["data"]["description"], - data["data"]["descriptionLocal"], - data["data"]["copyright"], + bible_id=bible_id, + abbreviation=data["data"]["abbreviation"], + language=data["data"]["language"]["name"], + abbreviation_local=data["data"]["abbreviationLocal"], + language_local=data["data"]["language"]["nameLocal"], + description=data["data"]["description"], + description_local=data["data"]["descriptionLocal"], + version_copyright=data["data"]["copyright"], ) async def _get_passage( @@ -135,16 +135,17 @@ class Bible(commands.Cog): response.status, ) if response.status == 400: - raise bible.errors.InexplicableError() + raise bible.errors.InexplicableError if response.status == 401: - raise bible.errors.Unauthorized() + raise bible.errors.UnauthorizedError if response.status == 403: - raise bible.errors.BibleAccessError() + raise bible.errors.BibleAccessError if response.status == 404: - raise bible.errors.NotFound() + raise bible.errors.NotFoundError if response.status == 503: - raise bible.errors.ServiceUnavailable() + raise bible.errors.ServiceUnavailableError + assert self.bot.user is not None # bot will always be logged in fums_url = "https://fums.api.bible/f3" fums_params = { "t": data["meta"]["fumsToken"], @@ -176,11 +177,11 @@ class Bible(commands.Cog): response.status, ) if response.status == 401: - raise bible.errors.Unauthorized() + raise bible.errors.UnauthorizedError if response.status == 403: - raise bible.errors.BibleAccessError() + raise bible.errors.BibleAccessError if response.status == 503: - raise bible.errors.ServiceUnavailable() + raise bible.errors.ServiceUnavailableError return data["data"] async def _get_chapters(self, bible_id: str, book_id: str) -> dict: @@ -195,11 +196,11 @@ class Bible(commands.Cog): response.status, ) if response.status == 401: - raise bible.errors.Unauthorized() + raise bible.errors.UnauthorizedError if response.status == 403: - raise bible.errors.BibleAccessError() + raise bible.errors.BibleAccessError if response.status == 503: - raise bible.errors.ServiceUnavailable() + raise bible.errors.ServiceUnavailableError return data["data"] async def _get_verses(self, bible_id: str, book_id: str, chapter: int) -> dict: @@ -214,11 +215,11 @@ class Bible(commands.Cog): response.status, ) if response.status == 401: - raise bible.errors.Unauthorized() + raise bible.errors.UnauthorizedError if response.status == 403: - raise bible.errors.BibleAccessError() + raise bible.errors.BibleAccessError if response.status == 503: - raise bible.errors.ServiceUnavailable() + raise bible.errors.ServiceUnavailableError return data["data"] @commands.group(autohelp=True) @@ -246,34 +247,34 @@ class Bible(commands.Cog): from_verse, to_verse = passage.replace(":", ".").split("-") if "." not in to_verse: to_verse = f"{from_verse.split('.')[0]}.{to_verse}" - passage = await self._get_passage(ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True) + retrieved_passage = await self._get_passage(ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True) else: - passage = await self._get_passage(ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False) + retrieved_passage = await self._get_passage(ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False) except ( bible.errors.BibleAccessError, - bible.errors.NotFound, + bible.errors.NotFoundError, bible.errors.InexplicableError, - bible.errors.ServiceUnavailable, - bible.errors.Unauthorized, + bible.errors.ServiceUnavailableError, + bible.errors.UnauthorizedError, ) as e: await ctx.send(e.message) return - if len(passage["content"]) > 4096: + if len(retrieved_passage["content"]) > 4096: await ctx.send("The passage is too long to send.") return if await ctx.embed_requested(): icon = self.get_icon(await ctx.embed_color()) embed = Embed( - title=f"{passage['reference']}", - description=passage["content"].replace("¶ ", ""), + title=f"{retrieved_passage['reference']}", + description=retrieved_passage["content"].replace("¶ ", ""), color=await ctx.embed_color(), ) - embed.set_footer(text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})", icon_url="attachment://icon.png") + embed.set_footer(text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviation_local} ({version.language_local}, {version.description_local})", icon_url="attachment://icon.png") await ctx.send(embed=embed, file=icon) else: - await ctx.send(f"## {passage['reference']}\n{passage['content']}") + await ctx.send(f"## {retrieved_passage['reference']}\n{retrieved_passage['content']}") @bible.command(name="random") async def bible_random(self, ctx: commands.Context): @@ -294,10 +295,10 @@ class Bible(commands.Cog): passage = await self._get_passage(ctx, bible_id, verse, False) except ( bible.errors.BibleAccessError, - bible.errors.NotFound, + bible.errors.NotFoundError, bible.errors.InexplicableError, - bible.errors.ServiceUnavailable, - bible.errors.Unauthorized, + bible.errors.ServiceUnavailableError, + bible.errors.UnauthorizedError, ) as e: await ctx.send(e.message) return @@ -309,7 +310,7 @@ class Bible(commands.Cog): description=passage["content"].replace("¶ ", ""), color=await ctx.embed_color(), ) - embed.set_footer(text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})", icon_url="attachment://icon.png") + embed.set_footer(text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviation_local} ({version.language_local}, {version.description_local})", icon_url="attachment://icon.png") await ctx.send(embed=embed, file=icon) else: await ctx.send(f"## {passage['reference']}\n{passage['content']}") diff --git a/bible/errors.py b/bible/errors.py index 708ca86..954e92d 100644 --- a/bible/errors.py +++ b/bible/errors.py @@ -10,7 +10,7 @@ class BibleAccessError(Exception): self.message = message -class Unauthorized(Exception): +class UnauthorizedError(Exception): def __init__( self, message: str = error("The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here]()."), @@ -19,7 +19,7 @@ class Unauthorized(Exception): self.message = message -class NotFound(Exception): +class NotFoundError(Exception): def __init__( self, message: str = error("The requested passage was not found."), @@ -28,7 +28,7 @@ class NotFound(Exception): self.message = message -class ServiceUnavailable(Exception): +class ServiceUnavailableError(Exception): def __init__( self, message: str = error("The API.Bible service is currently unavailable."), diff --git a/bible/info.json b/bible/info.json index b2da6d1..79ae088 100644 --- a/bible/info.json +++ b/bible/info.json @@ -1,18 +1,15 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing Bible!\nThis cog requires setting an API key for API.Bible. Please read the [documentation](https://seacogs.coastalcommits.com/bible/#setup) for more information.\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", - "name" : "Bible", - "short" : "Retrieve Bible verses from API.Bible.", - "description" : "Retrieve Bible verses from the API.Bible API. This cog requires an API.Bible api key.", - "end_user_data_statement" : "This cog does not store end user data, however it does send the following data to the API.Bible API:\n- The bot user's ID\n- The timestamp of the invoking message\n- The hashed user id of the invoking user", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing Bible!\nThis cog requires setting an API key for API.Bible. Please read the [documentation](https://seacogs.coastalcommits.com/bible/#setup) for more information.\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", + "name": "Bible", + "short": "Retrieve Bible verses from API.Bible.", + "description": "Retrieve Bible verses from the API.Bible API. This cog requires an API.Bible api key.", + "end_user_data_statement": "This cog does not store end user data, however it does send the following data to the API.Bible API:\n- The bot user's ID\n- The timestamp of the invoking message\n- The hashed user id of the invoking user", "hidden": false, "disabled": false, "min_bot_version": "3.5.0", "min_python_version": [3, 10, 0], "requirements": ["numpy", "pillow"], - "tags": [ - "fun", - "utility", - "api" - ] + "tags": ["fun", "utility", "api"] } diff --git a/bible/models.py b/bible/models.py index 97500be..b8e3b09 100644 --- a/bible/models.py +++ b/bible/models.py @@ -4,23 +4,23 @@ class Version: bible_id, abbreviation, language, - abbreviationLocal, - languageLocal, + abbreviation_local, + language_local, description, - descriptionLocal, + description_local, version_copyright, ): self.bible_id = bible_id self.abbreviation = abbreviation self.language = language - self.abbreviationLocal = abbreviationLocal - self.languageLocal = languageLocal + self.abbreviation_local = abbreviation_local + self.language_local = language_local self.description = description - self.descriptionLocal = descriptionLocal + self.description_local = description_local self.copyright = version_copyright def __str__(self): - return self.abbreviationLocal + return self.abbreviation_local def __repr__(self): - return f'bible.models.Version("{self.bible_id}", "{self.abbreviation}", "{self.language}", "{self.abbreviationLocal}", "{self.languageLocal}", "{self.description}", "{self.descriptionLocal}", "{self.copyright}")' + return f'bible.models.Version("{self.bible_id}", "{self.abbreviation}", "{self.language}", "{self.abbreviation_local}", "{self.language_local}", "{self.description}", "{self.description_local}", "{self.copyright}")' diff --git a/emojiinfo/emojiinfo.py b/emojiinfo/emojiinfo.py index 3dd8188..96b533d 100644 --- a/emojiinfo/emojiinfo.py +++ b/emojiinfo/emojiinfo.py @@ -16,13 +16,13 @@ class EmojiInfo(commands.Cog): __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "1.0.1" + __version__ = "1.0.3" __documentation__ = "https://seacogs.coastalcommits.com/emojiinfo/" def __init__(self, bot: Red) -> None: super().__init__() self.bot: Red = bot - self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.Emoji") + self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.EmojiInfo") def format_help_for_context(self, ctx: commands.Context) -> str: pre_processed = super().format_help_for_context(ctx) or "" @@ -35,14 +35,12 @@ class EmojiInfo(commands.Cog): ] return "\n".join(text) - async def fetch_twemoji(self, unicode_emoji) -> str: base_url = "https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/72x72/" emoji_codepoint = "-".join([hex(ord(char))[2:] for char in unicode_emoji]) segments = emoji_codepoint.split("-") valid_segments = [seg for seg in segments if len(seg) >= 4] - emoji_url = f"{base_url}{valid_segments[0]}.png" - return emoji_url + return f"{base_url}{valid_segments[0]}.png" async def fetch_primary_color(self, emoji_url: str) -> discord.Color | None: async with aiohttp.ClientSession() as session: @@ -51,8 +49,7 @@ class EmojiInfo(commands.Cog): return None image = await response.read() dominant_color = ColorThief(io.BytesIO(image)).get_color(quality=1) - color = discord.Color.from_rgb(*dominant_color) - return color + return discord.Color.from_rgb(*dominant_color) async def get_emoji_info(self, emoji: PartialEmoji) -> tuple[str, str]: if emoji.is_unicode_emoji(): @@ -72,59 +69,51 @@ class EmojiInfo(commands.Cog): else: emoji_id = "" markdown = f"`{emoji}`" - name = f"{bold('Name:')} {emoji.aliases.pop(0)}\n" + name = f"{bold('Name:')} {emoji.aliases.pop(0) if emoji.aliases else emoji.name}\n" aliases = f"{bold('Aliases:')} {', '.join(emoji.aliases)}\n" if emoji.aliases else "" group = f"{bold('Group:')} {emoji.group}\n" - return ( - f"{name}" - f"{emoji_id}" - f"{bold('Native:')} {emoji.is_unicode_emoji()}\n" - f"{group}" - f"{aliases}" - f"{bold('Animated:')} {emoji.animated}\n" - f"{bold('Markdown:')} {markdown}\n" - f"{bold('URL:')} [Click Here]({emoji_url})" - ), emoji_url + return (f"{name}{emoji_id}{bold('Native:')} {emoji.is_unicode_emoji()}\n{group}{aliases}{bold('Animated:')} {emoji.animated}\n{bold('Markdown:')} {markdown}\n{bold('URL:')} [Click Here]({emoji_url})"), emoji_url @app_commands.command(name="emoji") - @app_commands.describe( - emoji="What emoji would you like to get information on?", - ephemeral="Would you like the response to be hidden?" - ) + @app_commands.describe(emoji="What emoji would you like to get information on?", ephemeral="Would you like the response to be hidden?") async def emoji_slash(self, interaction: discord.Interaction, emoji: str, ephemeral: bool = True) -> None: """Retrieve information about an emoji.""" await interaction.response.defer(ephemeral=ephemeral) try: - emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji) - string, emoji_url, = await self.get_emoji_info(emoji) + retrieved_emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji) + string, emoji_url = await self.get_emoji_info(retrieved_emoji) self.logger.verbose(f"Emoji:\n{string}") except (IndexError, UnboundLocalError): return await interaction.followup.send("Please provide a valid emoji!") + assert isinstance(interaction.channel, discord.TextChannel) if await self.bot.embed_requested(channel=interaction.channel): - embed = embed = discord.Embed(title="Emoji Information", description=string, color = await self.fetch_primary_color(emoji_url) or await self.bot.get_embed_color(interaction.channel)) + embed = discord.Embed(title="Emoji Information", description=string, color=await self.fetch_primary_color(emoji_url) or await self.bot.get_embed_color(interaction.channel)) embed.set_thumbnail(url=emoji_url) await interaction.followup.send(embed=embed) - else: - await interaction.followup.send(content=string) + return None + await interaction.followup.send(content=string) + return None @commands.command(name="emoji") async def emoji(self, ctx: commands.Context, *, emoji: str) -> None: """Retrieve information about an emoji.""" try: - emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji) - string, emoji_url, = await self.get_emoji_info(emoji) + retrieved_emoji: PartialEmoji = PartialEmoji.from_str(self, value=emoji) + string, emoji_url = await self.get_emoji_info(retrieved_emoji) self.logger.verbose(f"Emoji:\n{string}") except (IndexError, UnboundLocalError): - return await ctx.send("Please provide a valid emoji!") + await ctx.send("Please provide a valid emoji!") + return if await ctx.embed_requested(): - embed = embed = discord.Embed(title="Emoji Information", description=string, color = await self.fetch_primary_color(emoji_url) or await ctx.embed_color) + embed = discord.Embed(title="Emoji Information", description=string, color=await self.fetch_primary_color(emoji_url) or await ctx.embed_color()) embed.set_thumbnail(url=emoji_url) await ctx.send(embed=embed) - else: - await ctx.send(content=string) + return + await ctx.send(content=string) + return diff --git a/emojiinfo/info.json b/emojiinfo/info.json index 68a8de1..ccc32c7 100644 --- a/emojiinfo/info.json +++ b/emojiinfo/info.json @@ -1,16 +1,15 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing Emoji!", - "name" : "Emoji", - "short" : "Retrieve information about emojis.", - "description" : "Retrieve information about emojis.", - "end_user_data_statement" : "This cog does not store end user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing Emoji!", + "name": "Emoji", + "short": "Retrieve information about emojis.", + "description": "Retrieve information about emojis.", + "end_user_data_statement": "This cog does not store end user data.", "hidden": false, "disabled": false, "min_bot_version": "3.5.0", "min_python_version": [3, 10, 0], "requirements": ["colorthief"], - "tags": [ - "utility" - ] + "tags": ["utility"] } diff --git a/emojiinfo/model.py b/emojiinfo/model.py index cc8a468..c553950 100644 --- a/emojiinfo/model.py +++ b/emojiinfo/model.py @@ -39,7 +39,7 @@ class PartialEmoji(discord.PartialEmoji): The group name of the emoji if it is a native emoji. """ - def __init__(self, *, name: str, animated: bool = False, id: int | None = None, group: str | None = None, aliases: list | None = None) -> None: # pylint: disable=redefined-builtin + def __init__(self, *, name: str, animated: bool = False, id: int | None = None, group: str | None = None, aliases: list | None = None) -> None: # pylint: disable=redefined-builtin # noqa: A002 super().__init__(name=name, animated=animated, id=id) self.group = group self.aliases = aliases @@ -72,12 +72,12 @@ class PartialEmoji(discord.PartialEmoji): match = cls._CUSTOM_EMOJI_RE.match(value) if match is not None: groups = match.groupdict() - animated = bool(groups['animated']) - emoji_id = int(groups['id']) - name = groups['name'] + animated = bool(groups["animated"]) + emoji_id = int(groups["id"]) + name = groups["name"] return cls(name=name, animated=animated, id=emoji_id) - path: data_manager.Path = data_manager.bundled_data_path(coginstance) / "emojis.json" + path = data_manager.bundled_data_path(coginstance) / "emojis.json" with open(path, "r", encoding="UTF-8") as file: emojis: dict = json.load(file) emoji_aliases = [] diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..363c464 --- /dev/null +++ b/flake.lock @@ -0,0 +1,25 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1738680400, + "narHash": "sha256-ooLh+XW8jfa+91F1nhf9OF7qhuA/y1ChLx6lXDNeY5U=", + "rev": "799ba5bffed04ced7067a91798353d360788b30d", + "revCount": 747653, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.747653%2Brev-799ba5bffed04ced7067a91798353d360788b30d/0194d302-29da-7009-8f43-5b8a58825954/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..819e209 --- /dev/null +++ b/flake.nix @@ -0,0 +1,72 @@ +{ + description = "SeaCogs Nix Flake"; + inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; + + outputs = + { self, nixpkgs }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forEachSupportedSystem = + f: + nixpkgs.lib.genAttrs supportedSystems ( + system: + f { + pkgs = import nixpkgs { inherit system; }; + lib = nixpkgs.lib; + } + ); + in + { + devShells = forEachSupportedSystem ( + { pkgs, lib }: + let + myPython = pkgs.python311; + lib-path = + with pkgs; + lib.makeLibraryPath [ + stdenv.cc.cc + # Red-DiscordBot dependencies + libffi + libsodium + # PyLav dependency + libaio + # Material for MkDocs dependency + cairo + ]; + in + { + default = pkgs.mkShell { + lib-path = lib-path; + packages = with pkgs; [ + myPython + uv + ruff # the ruff pip package installs a dynamically linked binary that cannot run on NixOS + forgejo-runner + # Red-DiscordBot dependencies + git + jdk17 + # Material for MkDocs dependencies + pngquant + # SeaCogs dependencies + dig + ]; + shellHook = # bash + '' + export "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${lib-path}" + export "UV_PYTHON_PREFERENCE=only-system" + export "UV_PYTHON_DOWNLOADS=never" + uv sync --all-groups + source ./.venv/bin/activate + export "PYTHONPATH=`pwd`/.venv/${myPython.sitePackages}/:$PYTHONPATH" + export "PATH=${pkgs.ruff}/bin:$PATH" + ''; + }; + } + ); + }; +} diff --git a/hotreload/hotreload.py b/hotreload/hotreload.py index d039a8a..268a41a 100644 --- a/hotreload/hotreload.py +++ b/hotreload/hotreload.py @@ -1,14 +1,19 @@ +import py_compile from asyncio import run_coroutine_threadsafe from pathlib import Path -from typing import Sequence +from tempfile import NamedTemporaryFile +from typing import Generator, List, Sequence +import discord from red_commons.logging import RedTraceLogger, getLogger -from redbot.core import commands +from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.core_commands import CoreLogic -from redbot.core.utils.chat_formatting import bold, humanize_list +from redbot.core.utils.chat_formatting import bold, box, humanize_list +from typing_extensions import override from watchdog.events import FileSystemEvent, FileSystemMovedEvent, RegexMatchingEventHandler from watchdog.observers import Observer +from watchdog.observers.api import BaseObserver class HotReload(commands.Cog): @@ -16,29 +21,34 @@ class HotReload(commands.Cog): __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "1.1.2" + __version__ = "1.4.1" __documentation__ = "https://seacogs.coastalcommits.com/hotreload/" def __init__(self, bot: Red) -> None: super().__init__() self.bot: Red = bot + self.config: Config = Config.get_conf(cog_instance=self, identifier=294518358420750336, force_registration=True) self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload") - self.observer = None + self.observers: List[BaseObserver] = [] + self.config.register_global(notify_channel=None, compile_before_reload=False) watchdog_loggers = [getLogger(name="watchdog.observers.inotify_buffer")] for watchdog_logger in watchdog_loggers: watchdog_logger.setLevel("INFO") # SHUT UP!!!! + @override async def cog_load(self) -> None: """Start the observer when the cog is loaded.""" - self.bot.loop.create_task(self.start_observer()) + _ = self.bot.loop.create_task(self.start_observer()) + @override async def cog_unload(self) -> None: """Stop the observer when the cog is unloaded.""" - if self.observer: - self.observer.stop() - self.observer.join() + for observer in self.observers: + observer.stop() + observer.join() self.logger.info("Stopped observer. No longer watching for file changes.") + @override def format_help_for_context(self, ctx: commands.Context) -> str: pre_processed = super().format_help_for_context(ctx) or "" n = "\n" if "\n\n" not in pre_processed else "" @@ -50,28 +60,66 @@ class HotReload(commands.Cog): ] return "\n".join(text) - async def get_paths(self) -> tuple[Path]: + async def get_paths(self) -> Generator[Path, None, None]: """Retrieve user defined paths.""" - cog_manager = self.bot._cog_mgr + cog_manager = self.bot._cog_mgr # noqa: SLF001 # We have to use this private method because there is no public API to get user defined paths cog_paths = await cog_manager.user_defined_paths() return (Path(path) for path in cog_paths) async def start_observer(self) -> None: """Start the observer to watch for file changes.""" - self.observer = Observer() + self.observers.append(Observer()) paths = await self.get_paths() - for path in paths: - self.observer.schedule(event_handler=HotReloadHandler(bot=self.bot, path=path), path=path, recursive=True) - self.observer.start() - self.logger.info("Started observer. Watching for file changes.") + is_first = True + for observer in self.observers: + if not is_first: + observer.stop() + observer.join() + self.logger.debug("Stopped hanging observer.") + continue + for path in paths: + if not path.exists(): + self.logger.warning("Path %s does not exist. Skipping.", path) + continue + self.logger.debug("Adding observer schedule for path %s.", path) + observer.schedule(event_handler=HotReloadHandler(cog=self, path=path), path=str(path), recursive=True) + observer.start() + self.logger.info("Started observer. Watching for file changes.") + is_first = False + + @checks.is_owner() + @commands.group(name="hotreload") + async def hotreload_group(self, ctx: commands.Context) -> None: + """HotReload configuration commands.""" + pass + + @hotreload_group.command(name="notifychannel") + async def hotreload_notifychannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: + """Set the channel to send notifications to.""" + await self.config.notify_channel.set(channel.id) + await ctx.send(f"Notifications will be sent to {channel.mention}.") + + @hotreload_group.command(name="compile") # type: ignore + async def hotreload_compile(self, ctx: commands.Context, compile_before_reload: bool) -> None: + """Set whether to compile modified files before reloading.""" + await self.config.compile_before_reload.set(compile_before_reload) + await ctx.send(f"I {'will' if compile_before_reload else 'will not'} compile modified files before hotreloading cogs.") + + @hotreload_group.command(name="list") # type: ignore + async def hotreload_list(self, ctx: commands.Context) -> None: + """List the currently active observers.""" + if not self.observers: + await ctx.send("No observers are currently active.") + return + await ctx.send(f"Currently active observers (If there are more than one of these, report an issue): {box(humanize_list([str(o) for o in self.observers], style='unit'))}") class HotReloadHandler(RegexMatchingEventHandler): """Handler for file changes.""" - def __init__(self, bot: Red, path: Path) -> None: + def __init__(self, cog: HotReload, path: Path) -> None: super().__init__(regexes=[r".*\.py$"]) - self.bot: Red = bot + self.cog: HotReload = cog self.path: Path = path self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload.Observer") @@ -84,26 +132,60 @@ class HotReloadHandler(RegexMatchingEventHandler): if event.event_type not in allowed_events: return - relative_src_path = Path(event.src_path).relative_to(self.path) + relative_src_path = Path(str(event.src_path)).relative_to(self.path) src_package_name = relative_src_path.parts[0] cogs_to_reload = [src_package_name] if isinstance(event, FileSystemMovedEvent): dest = f" to {event.dest_path}" - relative_dest_path = Path(event.dest_path).relative_to(self.path) + relative_dest_path = Path(str(event.dest_path)).relative_to(self.path) dest_package_name = relative_dest_path.parts[0] if dest_package_name != src_package_name: cogs_to_reload.append(dest_package_name) else: dest = "" - self.logger.info(f"File {event.src_path} has been {event.event_type}{dest}.") + self.logger.info("File %s has been %s%s.", event.src_path, event.event_type, dest) - run_coroutine_threadsafe(self.reload_cogs(cogs_to_reload), loop=self.bot.loop) + run_coroutine_threadsafe( + coro=self.reload_cogs( + cog_names=cogs_to_reload, + paths=[Path(str(p)) for p in (event.src_path, getattr(event, "dest_path", None)) if p], + ), + loop=self.cog.bot.loop, + ) - async def reload_cogs(self, cog_names: Sequence[str]) -> None: - """Reload modified cog.""" - core_logic = CoreLogic(bot=self.bot) - self.logger.info(f"Reloading cogs: {humanize_list(cog_names, style='unit')}") - await core_logic._reload(pkg_names=cog_names) - self.logger.info(f"Reloaded cogs: {humanize_list(cog_names, style='unit')}") + async def reload_cogs(self, cog_names: Sequence[str], paths: Sequence[Path]) -> None: + """Reload modified cogs.""" + if not self.compile_modified_files(cog_names, paths): + return + + core_logic = CoreLogic(bot=self.cog.bot) + self.logger.info("Reloading cogs: %s", humanize_list(cog_names, style="unit")) + await core_logic._reload(pkg_names=cog_names) # noqa: SLF001 # We have to use this private method because there is no public API to reload other cogs + self.logger.info("Reloaded cogs: %s", humanize_list(cog_names, style="unit")) + + channel = self.cog.bot.get_channel(await self.cog.config.notify_channel()) + if channel and isinstance(channel, discord.TextChannel): + await channel.send(f"Reloaded cogs: {humanize_list(cog_names, style='unit')}") + + def compile_modified_files(self, cog_names: Sequence[str], paths: Sequence[Path]) -> bool: + """Compile modified files to ensure they are valid Python files.""" + for path in paths: + if not path.exists() or path.suffix != ".py": + self.logger.debug("Path %s does not exist or does not point to a Python file. Skipping compilation step.", path) + continue + + try: + with NamedTemporaryFile() as temp_file: + self.logger.debug("Attempting to compile %s", path) + py_compile.compile(file=str(path), cfile=temp_file.name, doraise=True) + self.logger.debug("Successfully compiled %s", path) + + except py_compile.PyCompileError as e: + e.__suppress_context__ = True + self.logger.exception("%s failed to compile. Not reloading cogs %s.", path, humanize_list(cog_names, style="unit")) + return False + except OSError: + self.logger.exception("Failed to create tempfile for compilation step. Skipping.") + return True diff --git a/hotreload/info.json b/hotreload/info.json index 87dc851..4669078 100644 --- a/hotreload/info.json +++ b/hotreload/info.json @@ -1,17 +1,15 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing HotReload! This cog does not provide any commands, please see the [documentation](https://seacogs.coastalcommits.com/hotreload) for more information.", - "name" : "HotReload", - "short" : "Automatically reload cogs in local cog paths on file change.", - "description" : "Automatically reload cogs in local cog paths on file change.", - "end_user_data_statement" : "This cog does not store end user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing HotReload! Please see the [documentation](https://seacogs.coastalcommits.com/hotreload) to get started.", + "name": "HotReload", + "short": "Automatically reload cogs in local cog paths on file change.", + "description": "Automatically reload cogs in local cog paths on file change.", + "end_user_data_statement": "This cog does not store end user data.", "hidden": false, "disabled": false, "min_bot_version": "3.5.0", - "min_python_version": [3, 10, 0], + "min_python_version": [3, 8, 0], "requirements": ["watchdog"], - "tags": [ - "utility", - "development" - ] + "tags": ["utility", "development"] } diff --git a/info.json b/info.json index c679a52..08ba1dc 100644 --- a/info.json +++ b/info.json @@ -1,9 +1,8 @@ { - "author": [ - "cswimr" - ], - "install_msg": "Thanks for installing my repo!\n\nIf you have any issues with any of the cogs, please create an issue [here](https://coastalcommits.com/cswimr/SeaCogs/issues) or join my [Discord Server](https://discord.gg/eMUMe77Yb8 ).", - "index_name": "sea-cogs", - "short": "Various cogs for Red, by cswimr", - "description": "Various cogs for Red, by cswimr" + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thanks for installing my repo!\n\nIf you have any issues with any of the cogs, please create an issue [here](https://coastalcommits.com/cswimr/SeaCogs/issues) or join my [Discord Server](https://discord.gg/eMUMe77Yb8 ).", + "index_name": "sea-cogs", + "short": "Various cogs for Red, by cswimr", + "description": "Various cogs for Red, by cswimr" } diff --git a/mkdocs.yml b/mkdocs.yml index 9d61aee..81ab9ed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: SeaCogs Documentation -site_url: !ENV [SITE_URL, 'https://seacogs.coastalcommits.com'] +site_url: !ENV [SITE_URL, "https://seacogs.coastalcommits.com"] repo_name: CoastalCommits repo_url: https://coastalcommits.com/cswimr/SeaCogs -edit_uri: !ENV [EDIT_URI, 'src/branch/main/.docs'] +edit_uri: !ENV [EDIT_URI, "src/branch/main/.docs"] copyright: Copyright © 2023-2024, cswimr docs_dir: .docs @@ -12,20 +12,21 @@ site_description: Documentation for my Red-DiscordBot Cogs. nav: - Home: index.md - Aurora: - - aurora/index.md - - Moderation Commands: aurora/moderation-commands.md - - Case Commands: aurora/case-commands.md - - Configuration: aurora/configuration.md + - aurora/index.md + - Moderation Commands: aurora/moderation-commands.md + - Case Commands: aurora/case-commands.md + - Configuration: aurora/configuration.md - Bible: bible.md - Backup: backup.md - EmojiInfo: emojiinfo.md + - HotReload: hotreload.md - Nerdify: nerdify.md - Pterodactyl: - - pterodactyl/index.md - - Installing Red: pterodactyl/installing-red.md - - Getting Started: pterodactyl/getting-started.md - - Configuration: pterodactyl/configuration.md - - Regex Examples: pterodactyl/regex.md + - pterodactyl/index.md + - Installing Red: pterodactyl/installing-red.md + - Getting Started: pterodactyl/getting-started.md + - Configuration: pterodactyl/configuration.md + - Regex Examples: pterodactyl/regex.md plugins: - git-authors @@ -72,7 +73,7 @@ markdown_extensions: theme: name: material palette: - - media: '(prefers-color-scheme: light)' + - media: "(prefers-color-scheme: light)" scheme: default primary: white accent: light blue @@ -80,7 +81,7 @@ theme: icon: material/toggle-switch name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' + - media: "(prefers-color-scheme: dark)" scheme: slate primary: black accent: light blue diff --git a/nerdify/info.json b/nerdify/info.json index 1d06223..eeb0ef7 100644 --- a/nerdify/info.json +++ b/nerdify/info.json @@ -1,17 +1,14 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing Nerdify!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs). Based off of PhasecoreX's [UwU]() cog.", - "name" : "Nerdify", - "short" : "Nerdify your text!", - "description" : "Nerdify your text!", - "end_user_data_statement" : "This cog does not store end user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing Nerdify!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs). Based off of PhasecoreX's [UwU]() cog.", + "name": "Nerdify", + "short": "Nerdify your text!", + "description": "Nerdify your text!", + "end_user_data_statement": "This cog does not store end user data.", "hidden": false, "disabled": false, "min_bot_version": "3.5.0", "min_python_version": [3, 8, 0], - "tags": [ - "fun", - "text", - "meme" - ] + "tags": ["fun", "text", "meme"] } diff --git a/nerdify/nerdify.py b/nerdify/nerdify.py index f5e92b5..fb9b784 100644 --- a/nerdify/nerdify.py +++ b/nerdify/nerdify.py @@ -37,16 +37,20 @@ class Nerdify(commands.Cog): ] return "\n".join(text) - @commands.command(aliases=["nerd"]) async def nerdify( - self, ctx: commands.Context, *, text: Optional[str] = None + self, + ctx: commands.Context, + *, + text: Optional[str] = None, ) -> None: """Nerdify the replied to message, previous message, or your own text.""" if not text: if hasattr(ctx.message, "reference") and ctx.message.reference: with suppress( - discord.Forbidden, discord.NotFound, discord.HTTPException + discord.Forbidden, + discord.NotFound, + discord.HTTPException, ): message_id = ctx.message.reference.message_id if message_id: @@ -62,7 +66,9 @@ class Nerdify(commands.Cog): ctx.channel, self.nerdify_text(text), allowed_mentions=discord.AllowedMentions( - everyone=False, users=False, roles=False + everyone=False, + users=False, + roles=False, ), ) @@ -77,7 +83,10 @@ class Nerdify(commands.Cog): return f'"{text}" 🤓' async def type_message( - self, destination: discord.abc.Messageable, content: str, **kwargs: Any + self, + destination: discord.abc.Messageable, + content: str, + **kwargs: Any, ) -> Union[discord.Message, None]: """Simulate typing and sending a message to a destination. diff --git a/pterodactyl/info.json b/pterodactyl/info.json index 7c4545a..ec7c4db 100644 --- a/pterodactyl/info.json +++ b/pterodactyl/info.json @@ -1,19 +1,15 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing Pterodactyl!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).\nDocumentation can be found [here](https://seacogs.coastalcommits.com/pterodactyl ).", - "name" : "Pterodactyl", - "short" : "Interface with Pterodactyl through websockets.", - "description" : "Interface with Pterodactyl through websockets.", - "end_user_data_statement" : "This cog does not store end user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing Pterodactyl!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).\nDocumentation can be found [here](https://seacogs.coastalcommits.com/pterodactyl ).", + "name": "Pterodactyl", + "short": "Interface with Pterodactyl through websockets.", + "description": "Interface with Pterodactyl through websockets.", + "end_user_data_statement": "This cog does not store end user data.", "hidden": false, "disabled": false, "min_bot_version": "3.5.0", "min_python_version": [3, 8, 0], "requirements": ["git+https://github.com/cswimr/pydactyl", "websockets"], - "tags": [ - "pterodactyl", - "minecraft", - "server", - "management" - ] + "tags": ["pterodactyl", "minecraft", "server", "management"] } diff --git a/pterodactyl/logger.py b/pterodactyl/logger.py index 64b005f..8600903 100644 --- a/pterodactyl/logger.py +++ b/pterodactyl/logger.py @@ -1,8 +1,8 @@ from red_commons import logging from red_commons.logging import getLogger -logger = getLogger('red.SeaCogs.Pterodactyl') -websocket_logger = getLogger('red.SeaCogs.Pterodactyl.websocket') +logger = getLogger("red.SeaCogs.Pterodactyl") +websocket_logger = getLogger("red.SeaCogs.Pterodactyl.Websocket") if logger.level >= logging.VERBOSE: websocket_logger.setLevel(logging.logging.INFO) elif logger.level < logging.VERBOSE: diff --git a/pterodactyl/mcsrvstatus.py b/pterodactyl/mcsrvstatus.py index e51827a..0d06f64 100644 --- a/pterodactyl/mcsrvstatus.py +++ b/pterodactyl/mcsrvstatus.py @@ -3,8 +3,8 @@ import aiohttp async def get_status(host: str, port: int = 25565) -> tuple[bool, dict]: async with aiohttp.ClientSession() as session: - async with session.get(f'https://api.mcsrvstat.us/2/{host}:{port}') as response: - response = await response.json() - if response['online']: + async with session.get(f"https://api.mcsrvstat.us/2/{host}:{port}") as response: + response = await response.json() # noqa: PLW2901 + if response["online"]: return (True, response) return (False, response) diff --git a/pterodactyl/pterodactyl.py b/pterodactyl/pterodactyl.py index fc49e3f..99891e0 100644 --- a/pterodactyl/pterodactyl.py +++ b/pterodactyl/pterodactyl.py @@ -1,6 +1,6 @@ import asyncio import json -from typing import Mapping, Optional, Tuple, Union +from typing import AsyncIterable, Iterable, Mapping, Optional, Tuple, Union import discord import websockets @@ -9,8 +9,9 @@ from pydactyl import PterodactylClient from redbot.core import app_commands, commands from redbot.core.app_commands import Choice from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import bold, box, error, humanize_list +from redbot.core.utils.chat_formatting import bold, box, humanize_list from redbot.core.utils.views import ConfirmView +from typing_extensions import override from pterodactyl import mcsrvstatus from pterodactyl.config import config, register_config @@ -22,19 +23,20 @@ class Pterodactyl(commands.Cog): __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "2.0.4" + __version__ = "2.0.6" __documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/" def __init__(self, bot: Red): self.bot = bot self.client: Optional[PterodactylClient] = None self.task: Optional[asyncio.Task] = None - self.websocket: Optional[websockets.WebSocketClientProtocol] = None + self.websocket: Optional[websockets.ClientConnection] = None self.retry_counter: int = 0 register_config(config) - self.task = self.get_task() + self.task = self._get_task() self.update_topic.start() + @override def format_help_for_context(self, ctx: commands.Context) -> str: pre_processed = super().format_help_for_context(ctx) or "" n = "\n" if "\n\n" not in pre_processed else "" @@ -46,47 +48,57 @@ class Pterodactyl(commands.Cog): ] return "\n".join(text) + @override async def cog_load(self) -> None: pterodactyl_keys = await self.bot.get_shared_api_tokens("pterodactyl") api_key = pterodactyl_keys.get("api_key") if api_key is None: - self.task.cancel() - raise ValueError("Pterodactyl API key not set. Please set it using `[p]set api`.") + self.maybe_cancel_task() + logger.error("Pterodactyl API key not set. Please set it using `[p]set api`.") + return base_url = await config.base_url() if base_url is None: - self.task.cancel() - raise ValueError("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.") + self.maybe_cancel_task() + logger.error("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.") + return server_id = await config.server_id() if server_id is None: - self.task.cancel() - raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.") + self.maybe_cancel_task() + logger.error("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.") + return self.client = PterodactylClient(base_url, api_key).client + @override async def cog_unload(self) -> None: self.update_topic.cancel() - self.task.cancel() - self.retry_counter = 0 - await self.client._session.close() # pylint: disable=protected-access + self.maybe_cancel_task() - def get_task(self) -> asyncio.Task: + def maybe_cancel_task(self, reset_retry_counter: bool = True) -> None: + if self.task: + self.task.cancel() + if reset_retry_counter: + self.retry_counter = 0 + + def _get_task(self) -> asyncio.Task: from pterodactyl.websocket import establish_websocket_connection + task = self.bot.loop.create_task(establish_websocket_connection(self), name="Pterodactyl Websocket Connection") - task.add_done_callback(self.error_callback) + task.add_done_callback(self._error_callback) return task - def error_callback(self, fut) -> None: #NOTE - Thanks flame442 and zephyrkul for helping me figure this out + def _error_callback(self, fut) -> None: # NOTE Thanks flame442 and zephyrkul for helping me figure this out try: fut.result() except asyncio.CancelledError: logger.info("WebSocket task has been cancelled.") - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: # pylint: disable=broad-exception-caught logger.error("WebSocket task has failed: %s", e, exc_info=e) - self.task.cancel() + self.maybe_cancel_task(reset_retry_counter=False) if self.retry_counter < 5: self.retry_counter += 1 logger.info("Retrying in %s seconds...", 5 * self.retry_counter) - self.task = self.bot.loop.call_later(5 * self.retry_counter, self.get_task) + self.task = self.bot.loop.call_later(5 * self.retry_counter, self._get_task) else: logger.info("Retry limit reached. Stopping task.") @@ -97,9 +109,9 @@ class Pterodactyl(commands.Cog): console = self.bot.get_channel(await config.console_channel()) chat = self.bot.get_channel(await config.chat_channel()) if console: - await console.edit(topic=topic) + await console.edit(topic=topic) # type: ignore if chat: - await chat.edit(topic=topic) + await chat.edit(topic=topic) # type: ignore @commands.Cog.listener() async def on_message_without_command(self, message: discord.Message) -> None: @@ -110,13 +122,7 @@ class Pterodactyl(commands.Cog): return logger.debug("Received console command from %s: %s", message.author.id, message.content) await message.channel.send(f"Received console command from {message.author.id}: {message.content[:1900]}", allowed_mentions=discord.AllowedMentions.none()) - try: - await self.websocket.send(json.dumps({"event": "send command", "args": [message.content]})) - except websockets.exceptions.ConnectionClosed as e: - logger.error("WebSocket connection closed: %s", e) - self.task.cancel() - self.retry_counter = 0 - self.task = self.get_task() + await self._send(json.dumps({"event": "send command", "args": [message.content]})) if message.channel.id == await config.chat_channel() and message.author.bot is False: logger.debug("Received chat message from %s: %s", message.author.id, message.content) channel = self.bot.get_channel(await config.console_channel()) @@ -124,13 +130,22 @@ class Pterodactyl(commands.Cog): await channel.send(f"Received chat message from {message.author.id}: {message.content[:1900]}", allowed_mentions=discord.AllowedMentions.none()) msg = json.dumps({"event": "send command", "args": [await self.get_chat_command(message)]}) logger.debug("Sending chat message to server:\n%s", msg) + await self._send(message=msg) + + async def _send(self, message: Union[websockets.Data, Iterable[websockets.Data], AsyncIterable[websockets.Data]], text: bool = False): + """Send a message through the websocket connection. Restarts the websocket connection task if it is closed, and reinvokes itself.""" + try: + await self.websocket.send(message=message, text=text) # type: ignore - we want this to error if `self.websocket` is none + except websockets.exceptions.ConnectionClosed as e: + logger.error("WebSocket connection closed: %s", e) + self.maybe_cancel_task() + self.task = self._get_task() try: - await self.websocket.send(msg) - except websockets.exceptions.ConnectionClosed as e: - logger.error("WebSocket connection closed: %s", e) - self.task.cancel() - self.retry_counter = 0 - self.task = self.get_task() + await asyncio.wait_for(fut=self.task, timeout=60) + await self._send(message=message, text=text) + except asyncio.TimeoutError: + logger.error("Timeout while waiting for websocket connection") + raise async def get_topic(self) -> str: topic: str = await config.topic() @@ -141,23 +156,27 @@ class Pterodactyl(commands.Cog): if await config.api_endpoint() == "minecraft": status, response = await mcsrvstatus.get_status(await config.topic_hostname(), await config.topic_port()) if status: - placeholders.update({ - "I": response['ip'], - "M": str(response['players']['max']), - "P": str(response['players']['online']), - "V": response['version'], - "D": response['motd']['clean'][0] if response['motd']['clean'] else "unset", - }) + placeholders.update( + { + "I": response["ip"], + "M": str(response["players"]["max"]), + "P": str(response["players"]["online"]), + "V": response["version"], + "D": response["motd"]["clean"][0] if response["motd"]["clean"] else "unset", + }, + ) else: - placeholders.update({ - "I": response['ip'], - "M": "0", - "P": "0", - "V": "Server Offline", - "D": "Server Offline", - }) + placeholders.update( + { + "I": response["ip"], + "M": "0", + "P": "0", + "V": "Server Offline", + "D": "Server Offline", + }, + ) for key, value in placeholders.items(): - topic = topic.replace('.$' + key, value) + topic = topic.replace(".$" + key, value) return topic async def get_chat_command(self, message: discord.Message) -> str: @@ -166,42 +185,45 @@ class Pterodactyl(commands.Cog): "C": str(message.author.color), "D": message.author.discriminator, "I": str(message.author.id), - "M": message.content.replace('"','').replace("\n", " "), + "M": message.content.replace('"', "").replace("\n", " "), "N": message.author.display_name, "U": message.author.name, "V": await config.invite() or "use [p]pterodactyl config invite to change me", } for key, value in placeholders.items(): - command = command.replace('.$' + key, value) + command = command.replace(".$" + key, value) return command async def get_player_list(self) -> Optional[Tuple[str, list]]: if await config.api_endpoint() == "minecraft": status, response = await mcsrvstatus.get_status(await config.topic_hostname(), await config.topic_port()) - if status and 'list' in response['players']: - output_str = '\n'.join(response['players']['list']) - return output_str, response['players']['list'] + if status and "list" in response["players"]: + output_str = "\n".join(response["players"]["list"]) + return output_str, response["players"]["list"] return None + return None async def get_player_list_embed(self, ctx: Union[commands.Context, discord.Interaction]) -> Optional[discord.Embed]: player_list = await self.get_player_list() - if player_list: + if player_list and isinstance(ctx.channel, discord.abc.Messageable): embed = discord.Embed(color=await self.bot.get_embed_color(ctx.channel), title="Players Online") embed.description = player_list[0] return embed return None - async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = '') -> None: + async def power(self, ctx: Union[discord.Interaction, commands.Context], action: str, action_ing: str, warning: str = "") -> None: if isinstance(ctx, discord.Interaction): ctx = await self.bot.get_context(ctx) current_status = await config.current_status() if current_status == action_ing: - return await ctx.send(f"Server is already {action_ing}.", ephemeral=True) + await ctx.send(f"Server is already {action_ing}.", ephemeral=True) + return if current_status in ["starting", "stopping"] and action != "kill": - return await ctx.send("Another power action is already in progress.", ephemeral=True) + await ctx.send("Another power action is already in progress.", ephemeral=True) + return view = ConfirmView(ctx.author, disable_buttons=True) @@ -212,12 +234,13 @@ class Pterodactyl(commands.Cog): if view.result is True: await message.edit(content=f"Sending websocket command to {action} server...", view=None) - await self.websocket.send(json.dumps({"event": "set state", "args": [action]})) + await self._websocket_send(json.dumps({"event": "set state", "args": [action]})) await message.edit(content=f"Server {action_ing}", view=None) + return - else: - await message.edit(content="Cancelled.", view=None) + await message.edit(content="Cancelled.", view=None) + return async def send_command(self, ctx: Union[discord.Interaction, commands.Context], command: str): channel = self.bot.get_channel(await config.console_channel()) @@ -225,27 +248,19 @@ class Pterodactyl(commands.Cog): ctx = await self.bot.get_context(ctx) if channel: await channel.send(f"Received console command from {ctx.author.id}: {command[:1900]}", allowed_mentions=discord.AllowedMentions.none()) - try: - await self.websocket.send(json.dumps({"event": "send command", "args": [command]})) - await ctx.send(f"Command sent to server. {box(command, 'json')}") - except websockets.exceptions.ConnectionClosed as e: - logger.error("WebSocket connection closed: %s", e) - await ctx.send(error("WebSocket connection closed.")) - self.task.cancel() - self.retry_counter = 0 - self.task = self.get_task() + await self._websocket_send(json.dumps({"event": "send command", "args": [command]})) + await ctx.send(f"Command sent to server. {box(command, 'json')}") @commands.Cog.listener() - async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str,str]): # pylint: disable=unused-argument + async def on_red_api_tokens_update(self, service_name: str, api_tokens: Mapping[str, str]): # pylint: disable=unused-argument if service_name == "pterodactyl": logger.info("Configuration value set: api_key\nRestarting task...") - self.task.cancel() - self.retry_counter = 0 - self.task = self.get_task() + self.maybe_cancel_task(reset_retry_counter=True) + self.task = self._get_task() slash_pterodactyl = app_commands.Group(name="pterodactyl", description="Pterodactyl allows you to manage your Pterodactyl Panel from Discord.") - @slash_pterodactyl.command(name = "command", description = "Send a command to the server console.") + @slash_pterodactyl.command(name="command", description="Send a command to the server console.") async def slash_pterodactyl_command(self, interaction: discord.Interaction, command: str) -> None: """Send a command to the server console. @@ -255,7 +270,7 @@ class Pterodactyl(commands.Cog): The command to send to the server.""" return await self.send_command(interaction, command) - @slash_pterodactyl.command(name = "players", description = "Retrieve a list of players on the server.") + @slash_pterodactyl.command(name="players", description="Retrieve a list of players on the server.") async def slash_pterodactyl_players(self, interaction: discord.Interaction) -> None: """Retrieve a list of players on the server.""" e = await self.get_player_list_embed(interaction) @@ -264,13 +279,8 @@ class Pterodactyl(commands.Cog): else: await interaction.response.send_message("No players online.", ephemeral=True) - @slash_pterodactyl.command(name = "power", description = "Send power actions to the server.") - @app_commands.choices(action=[ - Choice(name="Start", value="start"), - Choice(name="Stop", value="stop"), - Choice(name="Restart", value="restart"), - Choice(name="⚠️ Kill ⚠️", value="kill") - ]) + @slash_pterodactyl.command(name="power", description="Send power actions to the server.") + @app_commands.choices(action=[Choice(name="Start", value="start"), Choice(name="Stop", value="stop"), Choice(name="Restart", value="restart"), Choice(name="⚠️ Kill ⚠️", value="kill")]) async def slash_pterodactyl_power(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None: """Send power actions to the server. @@ -284,11 +294,11 @@ class Pterodactyl(commands.Cog): return await self.power(interaction, action.value, "stopping...") return await self.power(interaction, action.value, f"{action.value}ing...") - @commands.group(autohelp = True, name = "pterodactyl", aliases = ["ptero"]) + @commands.group(autohelp=True, name="pterodactyl", aliases=["ptero"]) async def pterodactyl(self, ctx: commands.Context) -> None: """Pterodactyl allows you to manage your Pterodactyl Panel from Discord.""" - @pterodactyl.command(name = "players", aliases=["list", "online", "playerlist", "who"]) + @pterodactyl.command(name="players", aliases=["list", "online", "playerlist", "who"]) async def pterodactyl_players(self, ctx: commands.Context) -> None: """Retrieve a list of players on the server.""" e = await self.get_player_list_embed(ctx) @@ -297,43 +307,43 @@ class Pterodactyl(commands.Cog): else: await ctx.send("No players online.") - @pterodactyl.command(name = "command", aliases = ["cmd", "execute", "exec"]) + @pterodactyl.command(name="command", aliases=["cmd", "execute", "exec"]) @commands.admin() async def pterodactyl_command(self, ctx: commands.Context, *, command: str) -> None: """Send a command to the server console.""" return await self.send_command(ctx, command) - @pterodactyl.group(autohelp = True, name = "power") + @pterodactyl.group(autohelp=True, name="power") @commands.admin() async def pterodactyl_power(self, ctx: commands.Context) -> None: """Send power actions to the server.""" - @pterodactyl_power.command(name = "start") + @pterodactyl_power.command(name="start") async def pterodactyl_power_start(self, ctx: commands.Context) -> Optional[discord.Message]: """Start the server.""" return await self.power(ctx, "start", "starting...") - @pterodactyl_power.command(name = "stop") + @pterodactyl_power.command(name="stop") async def pterodactyl_power_stop(self, ctx: commands.Context) -> Optional[discord.Message]: """Stop the server.""" return await self.power(ctx, "stop", "stopping...") - @pterodactyl_power.command(name = "restart") + @pterodactyl_power.command(name="restart") async def pterodactyl_power_restart(self, ctx: commands.Context) -> Optional[discord.Message]: """Restart the server.""" return await self.power(ctx, "restart", "restarting...") - @pterodactyl_power.command(name = "kill") + @pterodactyl_power.command(name="kill") async def pterodactyl_power_kill(self, ctx: commands.Context) -> Optional[discord.Message]: """Kill the server.""" return await self.power(ctx, "kill", "stopping... (forcefully killed)", warning="**⚠️ Forcefully killing the server process can corrupt data in some cases. ⚠️**\n") - @pterodactyl.group(autohelp = True, name = "config", aliases = ["settings", "set"]) + @pterodactyl.group(autohelp=True, name="config", aliases=["settings", "set"]) @commands.is_owner() async def pterodactyl_config(self, ctx: commands.Context) -> None: """Configure Pterodactyl settings.""" - @pterodactyl_config.command(name = "url") + @pterodactyl_config.command(name="url") async def pterodactyl_config_base_url(self, ctx: commands.Context, *, base_url: str) -> None: """Set the base URL of your Pterodactyl Panel. @@ -342,59 +352,57 @@ class Pterodactyl(commands.Cog): await config.base_url.set(base_url) await ctx.send(f"Base URL set to {base_url}") logger.info("Configuration value set: base_url = %s\nRestarting task...", base_url) - self.task.cancel() - self.retry_counter = 0 - self.task = self.get_task() + self.maybe_cancel_task(reset_retry_counter=True) + self.task = self._get_task() - @pterodactyl_config.command(name = "serverid") + @pterodactyl_config.command(name="serverid") async def pterodactyl_config_server_id(self, ctx: commands.Context, *, server_id: str) -> None: """Set the ID of your server.""" await config.server_id.set(server_id) await ctx.send(f"Server ID set to {server_id}") logger.info("Configuration value set: server_id = %s\nRestarting task...", server_id) - self.task.cancel() - self.retry_counter = 0 - self.task = self.get_task() + self.maybe_cancel_task(reset_retry_counter=True) + self.task = self._get_task() - @pterodactyl_config.group(name = "console") + @pterodactyl_config.group(name="console") async def pterodactyl_config_console(self, ctx: commands.Context): """Configure console settings.""" - @pterodactyl_config_console.command(name = "channel") + @pterodactyl_config_console.command(name="channel") async def pterodactyl_config_console_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: """Set the channel to send console output to.""" await config.console_channel.set(channel.id) await ctx.send(f"Console channel set to {channel.mention}") - @pterodactyl_config_console.command(name = "commands") + @pterodactyl_config_console.command(name="commands") async def pterodactyl_config_console_commands(self, ctx: commands.Context, enabled: bool) -> None: """Enable or disable console commands.""" await config.console_commands_enabled.set(enabled) await ctx.send(f"Console commands set to {enabled}") - @pterodactyl_config.command(name = "invite") + @pterodactyl_config.command(name="invite") async def pterodactyl_config_invite(self, ctx: commands.Context, invite: str) -> None: """Set the invite link for your server.""" await config.invite.set(invite) await ctx.send(f"Invite link set to {invite}") - @pterodactyl_config.group(name = "topic") + @pterodactyl_config.group(name="topic") async def pterodactyl_config_topic(self, ctx: commands.Context): """Set the topic for the console and chat channels.""" - @pterodactyl_config_topic.command(name = "host", aliases = ["hostname", "ip"]) + @pterodactyl_config_topic.command(name="host", aliases=["hostname", "ip"]) async def pterodactyl_config_topic_host(self, ctx: commands.Context, host: str) -> None: """Set the hostname or IP address of your server.""" await config.topic_hostname.set(host) await ctx.send(f"Hostname/IP set to `{host}`") - @pterodactyl_config_topic.command(name = "port") + @pterodactyl_config_topic.command(name="port") async def pterodactyl_config_topic_port(self, ctx: commands.Context, port: int) -> None: """Set the port of your server.""" await config.topic_port.set(port) await ctx.send(f"Port set to `{port}`") - @pterodactyl_config_topic.command(name = "text") + @pterodactyl_config_topic.command(name="text") async def pterodactyl_config_topic_text(self, ctx: commands.Context, *, text: str) -> None: """Set the text for the console and chat channels. @@ -410,17 +418,17 @@ class Pterodactyl(commands.Cog): await config.topic.set(text) await ctx.send(f"Topic set to:\n{box(text, 'yaml')}") - @pterodactyl_config.group(name = "chat") + @pterodactyl_config.group(name="chat") async def pterodactyl_config_chat(self, ctx: commands.Context): """Configure chat settings.""" - @pterodactyl_config_chat.command(name = "channel") + @pterodactyl_config_chat.command(name="channel") async def pterodactyl_config_chat_channel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: """Set the channel to send chat output to.""" await config.chat_channel.set(channel.id) await ctx.send(f"Chat channel set to {channel.mention}") - @pterodactyl_config_chat.command(name = "command") + @pterodactyl_config_chat.command(name="command") async def pterodactyl_config_chat_command(self, ctx: commands.Context, *, command: str) -> None: """Set the command that will be used to send messages from Discord. @@ -429,11 +437,11 @@ class Pterodactyl(commands.Cog): await config.chat_command.set(command) await ctx.send(f"Chat command set to:\n{box(command, 'json')}") - @pterodactyl_config.group(name = "regex") + @pterodactyl_config.group(name="regex") async def pterodactyl_config_regex(self, ctx: commands.Context) -> None: """Set regex patterns.""" - @pterodactyl_config_regex.command(name = "chat") + @pterodactyl_config_regex.command(name="chat") async def pterodactyl_config_regex_chat(self, ctx: commands.Context, *, regex: str) -> None: """Set the regex pattern to match chat messages on the server. @@ -441,7 +449,7 @@ class Pterodactyl(commands.Cog): await config.chat_regex.set(regex) await ctx.send(f"Chat regex set to:\n{box(regex, 'regex')}") - @pterodactyl_config_regex.command(name = "server") + @pterodactyl_config_regex.command(name="server") async def pterodactyl_config_regex_server(self, ctx: commands.Context, *, regex: str) -> None: """Set the regex pattern to match server messages on the server. @@ -449,7 +457,7 @@ class Pterodactyl(commands.Cog): await config.server_regex.set(regex) await ctx.send(f"Server regex set to:\n{box(regex, 'regex')}") - @pterodactyl_config_regex.command(name = "join") + @pterodactyl_config_regex.command(name="join") async def pterodactyl_config_regex_join(self, ctx: commands.Context, *, regex: str) -> None: """Set the regex pattern to match join messages on the server. @@ -457,7 +465,7 @@ class Pterodactyl(commands.Cog): await config.join_regex.set(regex) await ctx.send(f"Join regex set to:\n{box(regex, 'regex')}") - @pterodactyl_config_regex.command(name = "leave") + @pterodactyl_config_regex.command(name="leave") async def pterodactyl_config_regex_leave(self, ctx: commands.Context, *, regex: str) -> None: """Set the regex pattern to match leave messages on the server. @@ -465,7 +473,7 @@ class Pterodactyl(commands.Cog): await config.leave_regex.set(regex) await ctx.send(f"Leave regex set to:\n{box(regex, 'regex')}") - @pterodactyl_config_regex.command(name = "achievement") + @pterodactyl_config_regex.command(name="achievement") async def pterodactyl_config_regex_achievement(self, ctx: commands.Context, *, regex: str) -> None: """Set the regex pattern to match achievement messages on the server. @@ -473,41 +481,41 @@ class Pterodactyl(commands.Cog): await config.achievement_regex.set(regex) await ctx.send(f"Achievement regex set to:\n{box(regex, 'regex')}") - @pterodactyl_config.group(name = "messages", aliases = ['msg', 'msgs', 'message']) + @pterodactyl_config.group(name="messages", aliases=["msg", "msgs", "message"]) async def pterodactyl_config_messages(self, ctx: commands.Context): """Configure message settings.""" - @pterodactyl_config_messages.command(name = "startup") + @pterodactyl_config_messages.command(name="startup") async def pterodactyl_config_messages_startup(self, ctx: commands.Context, *, message: str) -> None: """Set the message that will be sent when the server starts.""" await config.startup_msg.set(message) await ctx.send(f"Startup message set to: {message}") - @pterodactyl_config_messages.command(name = "shutdown") + @pterodactyl_config_messages.command(name="shutdown") async def pterodactyl_config_messages_shutdown(self, ctx: commands.Context, *, message: str) -> None: """Set the message that will be sent when the server stops.""" await config.shutdown_msg.set(message) await ctx.send(f"Shutdown message set to: {message}") - @pterodactyl_config_messages.command(name = "join") + @pterodactyl_config_messages.command(name="join") async def pterodactyl_config_messages_join(self, ctx: commands.Context, *, message: str) -> None: """Set the message that will be sent when a user joins the server. This is only shown in embeds.""" await config.join_msg.set(message) await ctx.send(f"Join message set to: {message}") - @pterodactyl_config_messages.command(name = "leave") + @pterodactyl_config_messages.command(name="leave") async def pterodactyl_config_messages_leave(self, ctx: commands.Context, *, message: str) -> None: """Set the message that will be sent when a user leaves the server. This is only shown in embeds.""" await config.leave_msg.set(message) await ctx.send(f"Leave message set to: {message}") - @pterodactyl_config.command(name = "ip") + @pterodactyl_config.command(name="ip") async def pterodactyl_config_mask_ip(self, ctx: commands.Context, mask: bool) -> None: """Mask the IP addresses of users in console messages.""" await config.mask_ip.set(mask) await ctx.send(f"IP masking set to {mask}") - @pterodactyl_config.command(name = "api") + @pterodactyl_config.command(name="api") async def pterodactyl_config_api(self, ctx: commands.Context, endpoint: str) -> None: """Set the API endpoint for retrieving user avatars. @@ -516,11 +524,14 @@ class Pterodactyl(commands.Cog): await config.api_endpoint.set(endpoint) await ctx.send(f"API endpoint set to {endpoint}") - @pterodactyl_config_regex.group(name = "blacklist", aliases = ['block', 'blocklist'],) + @pterodactyl_config_regex.group( + name="blacklist", + aliases=["block", "blocklist"], + ) async def pterodactyl_config_regex_blacklist(self, ctx: commands.Context): """Blacklist regex patterns.""" - @pterodactyl_config_regex_blacklist.command(name = "add") + @pterodactyl_config_regex_blacklist.command(name="add") async def pterodactyl_config_regex_blacklist_add(self, ctx: commands.Context, name: str, *, regex: str) -> None: """Add a regex pattern to the blacklist.""" async with config.regex_blacklist() as blacklist: @@ -538,7 +549,7 @@ class Pterodactyl(commands.Cog): else: await msg.edit(content="Cancelled.") - @pterodactyl_config_regex_blacklist.command(name = "remove") + @pterodactyl_config_regex_blacklist.command(name="remove") async def pterodactyl_config_regex_blacklist_remove(self, ctx: commands.Context, name: str) -> None: """Remove a regex pattern from the blacklist.""" async with config.regex_blacklist() as blacklist: @@ -555,7 +566,7 @@ class Pterodactyl(commands.Cog): else: await ctx.send(f"Name `{name}` does not exist in the blacklist.") - @pterodactyl_config.command(name = 'view', aliases = ['show']) + @pterodactyl_config.command(name="view", aliases=["show"]) async def pterodactyl_config_view(self, ctx: commands.Context) -> None: """View the current configuration.""" base_url = await config.base_url() @@ -580,7 +591,7 @@ class Pterodactyl(commands.Cog): topic_text = await config.topic() topic_hostname = await config.topic_hostname() topic_port = await config.topic_port() - embed = discord.Embed(color = await ctx.embed_color(), title="Pterodactyl Configuration") + embed = discord.Embed(color=await ctx.embed_color(), title="Pterodactyl Configuration") embed.description = f"""**Base URL:** {base_url} **Server ID:** `{server_id}` **Console Channel:** <#{console_channel}> @@ -596,19 +607,19 @@ class Pterodactyl(commands.Cog): **Topic Hostname:** `{topic_hostname}` **Topic Port:** `{topic_port}` - **Topic Text:** {box(topic_text, 'yaml')} + **Topic Text:** {box(topic_text, "yaml")} - **Chat Command:** {box(chat_command, 'json')} - **Chat Regex:** {box(chat_regex, 're')} - **Server Regex:** {box(server_regex, 're')} - **Join Regex:** {box(join_regex, 're')} - **Leave Regex:** {box(leave_regex, 're')} - **Achievement Regex:** {box(achievement_regex, 're')}""" + **Chat Command:** {box(chat_command, "json")} + **Chat Regex:** {box(chat_regex, "re")} + **Server Regex:** {box(server_regex, "re")} + **Join Regex:** {box(join_regex, "re")} + **Leave Regex:** {box(leave_regex, "re")} + **Achievement Regex:** {box(achievement_regex, "re")}""" await ctx.send(embed=embed) if not len(regex_blacklist) == 0: - regex_blacklist_embed = discord.Embed(color = await ctx.embed_color(), title="Regex Blacklist") + regex_blacklist_embed = discord.Embed(color=await ctx.embed_color(), title="Regex Blacklist") for name, regex in regex_blacklist.items(): - regex_blacklist_embed.add_field(name=name, value=box(regex, 're'), inline=False) + regex_blacklist_embed.add_field(name=name, value=box(regex, "re"), inline=False) await ctx.send(embed=regex_blacklist_embed) def get_bool_str(self, inp: bool) -> str: diff --git a/pterodactyl/websocket.py b/pterodactyl/websocket.py index e5fd8db..6ead074 100644 --- a/pterodactyl/websocket.py +++ b/pterodactyl/websocket.py @@ -2,14 +2,14 @@ import json import re from pathlib import Path -from typing import Optional, Tuple, Union +from typing import Any, Optional, Tuple, Union import aiohttp import discord -import websockets from pydactyl import PterodactylClient from redbot.core.data_manager import bundled_data_path from redbot.core.utils.chat_formatting import bold, pagify +from websockets.asyncio.client import connect from pterodactyl.config import config from pterodactyl.logger import logger, websocket_logger @@ -19,46 +19,48 @@ from pterodactyl.pterodactyl import Pterodactyl async def establish_websocket_connection(coginstance: Pterodactyl) -> None: await coginstance.bot.wait_until_red_ready() base_url = await config.base_url() - base_url = base_url[:-1] if base_url.endswith('/') else base_url + base_url = base_url[:-1] if base_url.endswith("/") else base_url logger.info("Establishing WebSocket connection") websocket_credentials = await retrieve_websocket_credentials(coginstance) - async with websockets.connect(websocket_credentials['data']['socket'], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket: + async with connect(websocket_credentials["data"]["socket"], origin=base_url, ping_timeout=60, logger=websocket_logger) as websocket: logger.info("WebSocket connection established") - auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]}) + auth_message = json.dumps({"event": "auth", "args": [websocket_credentials["data"]["token"]]}) await websocket.send(auth_message) logger.info("Authentication message sent") coginstance.websocket = websocket - while True: # pylint: disable=too-many-nested-blocks + while True: # pylint: disable=too-many-nested-blocks message = json.loads(await websocket.recv()) - if message['event'] in ('token expiring', 'token expired'): + if message["event"] in ("token expiring", "token expired"): logger.info("Received token expiring/expired event. Refreshing token.") websocket_credentials = await retrieve_websocket_credentials(coginstance) - auth_message = json.dumps({"event": "auth", "args": [websocket_credentials['data']['token']]}) + auth_message = json.dumps({"event": "auth", "args": [websocket_credentials["data"]["token"]]}) await websocket.send(auth_message) logger.info("Authentication message sent") - if message['event'] == 'auth success': + if message["event"] == "auth success": logger.info("WebSocket authentication successful") - if message['event'] == 'console output' and await config.console_channel() is not None: + if message["event"] == "console output" and await config.console_channel() is not None: regex_blacklist: dict = await config.regex_blacklist() - matches = [re.search(regex, message['args'][0]) for regex in regex_blacklist.values()] + matches = [re.search(regex, message["args"][0]) for regex in regex_blacklist.values()] - if await config.current_status() in ('running', '') and not any(matches): - content = remove_ansi_escape_codes(message['args'][0]) + if await config.current_status() in ("running", "") and not any(matches): + content = remove_ansi_escape_codes(message["args"][0]) if await config.mask_ip() is True: content = mask_ip(content) console_channel = coginstance.bot.get_channel(await config.console_channel()) + assert isinstance(console_channel, discord.abc.Messageable) chat_channel = coginstance.bot.get_channel(await config.chat_channel()) + assert isinstance(chat_channel, discord.abc.Messageable) if console_channel is not None: - if content.startswith('['): + if content.startswith("["): pagified_content = pagify(content, delims=[" ", "\n"]) for page in pagified_content: await console_channel.send(content=page, allowed_mentions=discord.AllowedMentions.none()) @@ -66,24 +68,24 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None: server_message = await check_if_server_message(content) if server_message: if chat_channel is not None: - await chat_channel.send(server_message if len(server_message) < 2000 else server_message[:1997] + '...', allowed_mentions=discord.AllowedMentions.none()) + await chat_channel.send(server_message if len(server_message) < 2000 else server_message[:1997] + "...", allowed_mentions=discord.AllowedMentions.none()) chat_message = await check_if_chat_message(content) if chat_message: - info = await get_info(chat_message['username']) + info = await get_info(chat_message["username"]) if info is not None: - await send_chat_discord(coginstance, chat_message['username'], chat_message['message'], info['data']['player']['avatar']) + await send_chat_discord(coginstance, chat_message["username"], chat_message["message"], info["data"]["player"]["avatar"]) else: - await send_chat_discord(coginstance, chat_message['username'], chat_message['message'], 'https://seafsh.cc/u/j3AzqQ.png') + await send_chat_discord(coginstance, chat_message["username"], chat_message["message"], "https://seafsh.cc/u/j3AzqQ.png") join_message = await check_if_join_message(content) if join_message: if chat_channel is not None: if coginstance.bot.embed_requested(chat_channel): - embed, img = await generate_join_leave_embed(coginstance=coginstance, username=join_message,join=True) + embed, img = await generate_join_leave_embed(coginstance=coginstance, username=join_message, join=True) if img: - with open(img, 'rb') as file: - await chat_channel.send(embed=embed, file=file) + with open(img, "rb") as file: + await chat_channel.send(embed=embed, file=discord.File(fp=file)) else: await chat_channel.send(embed=embed) else: @@ -93,10 +95,10 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None: if leave_message: if chat_channel is not None: if coginstance.bot.embed_requested(chat_channel): - embed, img = await generate_join_leave_embed(coginstance=coginstance, username=leave_message,join=False) + embed, img = await generate_join_leave_embed(coginstance=coginstance, username=leave_message, join=False) if img: - with open(img, 'rb') as file: - await chat_channel.send(embed=embed, file=file) + with open(img, "rb") as file: + await chat_channel.send(embed=embed, file=discord.File(fp=file)) else: await chat_channel.send(embed=embed) else: @@ -106,13 +108,17 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None: if achievement_message: if chat_channel is not None: if coginstance.bot.embed_requested(chat_channel): - await chat_channel.send(embed=await generate_achievement_embed(coginstance, achievement_message['username'], achievement_message['achievement'], achievement_message['challenge'])) + embed, img = await generate_achievement_embed(coginstance, achievement_message["username"], achievement_message["achievement"], achievement_message["challenge"]) + if img: + await chat_channel.send(embed=embed, file=discord.File(fp=img)) + else: + await chat_channel.send(embed=embed) else: await chat_channel.send(f"{achievement_message['username']} has {'completed the challenge' if achievement_message['challenge'] else 'made the advancement'} {achievement_message['achievement']}") - if message['event'] == 'status': + if message["event"] == "status": old_status = await config.current_status() - current_status = message['args'][0] + current_status = message["args"][0] if old_status != current_status: await config.current_status.set(current_status) if await config.console_channel() is not None: @@ -120,81 +126,92 @@ async def establish_websocket_connection(coginstance: Pterodactyl) -> None: if console is not None: await console.send(f"Server status changed! `{current_status}`") if await config.chat_channel() is not None: - if current_status == 'running' and await config.startup_msg() is not None: + if current_status == "running" and await config.startup_msg() is not None: chat = coginstance.bot.get_channel(await config.chat_channel()) if chat is not None: await chat.send(await config.startup_msg()) - if current_status == 'stopping' and await config.shutdown_msg() is not None: + if current_status == "stopping" and await config.shutdown_msg() is not None: chat = coginstance.bot.get_channel(await config.chat_channel()) if chat is not None: await chat.send(await config.shutdown_msg()) -async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> Optional[dict]: + +async def retrieve_websocket_credentials(coginstance: Pterodactyl) -> dict: pterodactyl_keys = await coginstance.bot.get_shared_api_tokens("pterodactyl") api_key = pterodactyl_keys.get("api_key") if api_key is None: - coginstance.task.cancel() + coginstance.maybe_cancel_task() raise ValueError("Pterodactyl API key not set. Please set it using `[p]set api`.") base_url = await config.base_url() if base_url is None: - coginstance.task.cancel() + coginstance.maybe_cancel_task() raise ValueError("Pterodactyl base URL not set. Please set it using `[p]pterodactyl config url`.") server_id = await config.server_id() if server_id is None: - coginstance.task.cancel() + coginstance.maybe_cancel_task() raise ValueError("Pterodactyl server ID not set. Please set it using `[p]pterodactyl config serverid`.") client = PterodactylClient(base_url, api_key).client coginstance.client = client - websocket_credentials = client.servers.get_websocket(server_id) - logger.debug("""Websocket connection details retrieved: + websocket_credentials: dict[str, Any] = client.servers.get_websocket(server_id).json() + if not websocket_credentials: + coginstance.maybe_cancel_task() + raise ValueError("Failed to retrieve websocket credentials. Please ensure the API details are correctly configured.") + logger.debug( + """Websocket connection details retrieved: Socket: %s Token: %s...""", - websocket_credentials['data']['socket'], - websocket_credentials['data']['token'][:20] - ) + websocket_credentials["data"]["socket"], + websocket_credentials["data"]["token"][:20], + ) return websocket_credentials - #NOTE - The token is truncated to prevent it from being logged in its entirety, for security reasons + # NOTE - The token is truncated to prevent it from being logged in its entirety, for security reasons + def remove_ansi_escape_codes(text: str) -> str: - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') - #NOTE - https://chat.openai.com/share/d92f9acf-d776-4fd6-a53f-b14ac15dd540 - return ansi_escape.sub('', text) + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + # NOTE - https://chat.openai.com/share/d92f9acf-d776-4fd6-a53f-b14ac15dd540 + return ansi_escape.sub("", text) -async def check_if_server_message(text: str) -> Union[bool, str]: + +async def check_if_server_message(text: str) -> Optional[str]: regex = await config.server_regex() match: Optional[re.Match[str]] = re.match(regex, text) if match: logger.trace("Message is a server message") return match.group(1) - return False + return None -async def check_if_chat_message(text: str) -> Union[bool, dict]: + +async def check_if_chat_message(text: str) -> Optional[dict]: regex = await config.chat_regex() match: Optional[re.Match[str]] = re.match(regex, text) if match: groups = {"username": match.group(1), "message": match.group(2)} logger.trace("Message is a chat message\n%s", json.dumps(groups)) return groups - return False + return None -async def check_if_join_message(text: str) -> Union[bool, str]: + +async def check_if_join_message(text: str) -> Optional[str]: regex = await config.join_regex() match: Optional[re.Match[str]] = re.match(regex, text) if match: logger.trace("Message is a join message") return match.group(1) - return False + return None -async def check_if_leave_message(text: str) -> Union[bool, str]: + +async def check_if_leave_message(text: str) -> Optional[str]: regex = await config.leave_regex() match: Optional[re.Match[str]] = re.match(regex, text) if match: logger.trace("Message is a leave message") return match.group(1) - return False + return None -async def check_if_achievement_message(text: str) -> Union[bool, dict]: + +async def check_if_achievement_message(text: str) -> Optional[dict]: regex = await config.achievement_regex() match: Optional[re.Match[str]] = re.match(regex, text) if match: @@ -205,7 +222,8 @@ async def check_if_achievement_message(text: str) -> Union[bool, dict]: groups["challenge"] = False logger.trace("Message is an achievement message") return groups - return False + return None + async def get_info(username: str) -> Optional[dict]: logger.verbose("Retrieving player info for %s", username) @@ -218,6 +236,7 @@ async def get_info(username: str) -> Optional[dict]: logger.warning("Failed to retrieve player info for %s: %s", username, response.status) return None + async def send_chat_discord(coginstance: Pterodactyl, username: str, message: str, avatar_url: str) -> None: logger.trace("Sending chat message to Discord") channel = coginstance.bot.get_channel(await config.chat_channel()) @@ -231,6 +250,7 @@ async def send_chat_discord(coginstance: Pterodactyl, username: str, message: st else: logger.warning("Chat channel not set. Skipping sending chat message to Discord") + async def generate_join_leave_embed(coginstance: Pterodactyl, username: str, join: bool) -> Tuple[discord.Embed, Optional[Union[str, Path]]]: embed = discord.Embed() embed.color = discord.Color.green() if join else discord.Color.red() @@ -238,30 +258,32 @@ async def generate_join_leave_embed(coginstance: Pterodactyl, username: str, joi info = await get_info(username) if info: img = None - embed.set_author(name=username, icon_url=info['data']['player']['avatar']) + embed.set_author(name=username, icon_url=info["data"]["player"]["avatar"]) else: img = bundled_data_path(coginstance) / "unknown.png" - embed.set_author(name=username, icon_url='attachment://unknown.png') + embed.set_author(name=username, icon_url="attachment://unknown.png") embed.timestamp = discord.utils.utcnow() return embed, img + async def generate_achievement_embed(coginstance: Pterodactyl, username: str, achievement: str, challenge: bool) -> Tuple[discord.Embed, Optional[Union[str, Path]]]: embed = discord.Embed() - embed.color = discord.Color.from_str('#a800a7') if challenge else discord.Color.from_str('#54fb54') + embed.color = discord.Color.from_str("#a800a7") if challenge else discord.Color.from_str("#54fb54") embed.description = f"{bold(username)} has {'completed the challenge' if challenge else 'made the advancement'} {bold(achievement)}" info = await get_info(username) if info: img = None - embed.set_author(name=username, icon_url=info['data']['player']['avatar']) + embed.set_author(name=username, icon_url=info["data"]["player"]["avatar"]) else: img = bundled_data_path(coginstance) / "unknown.png" - embed.set_author(name=username, icon_url='attachment://unknown.png') + embed.set_author(name=username, icon_url="attachment://unknown.png") embed.timestamp = discord.utils.utcnow() return embed, img + def mask_ip(string: str) -> str: def check(match: re.Match[str]): ip = match.group(0) - masked_ip = '.'.join(r'\*' * len(octet) for octet in ip.split('.')) - return masked_ip - return re.sub(r'\b(?:\d{1,3}\.){3}\d{1,3}\b', check, string) + return ".".join(r"\*" * len(octet) for octet in ip.split(".")) + + return re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", check, string) diff --git a/pyproject.toml b/pyproject.toml index 80a28bc..8dce5ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,75 +2,75 @@ name = "seacogs" version = "0.1.0" description = "My assorted cogs for Red-DiscordBot." -authors = [{name = "cswimr", email = "seaswimmerthefsh@gmail.com"}] -license = {file="LICENSE"} +authors = [{ name = "cswimr", email = "seaswimmerthefsh@gmail.com" }] +license = { file = "LICENSE" } readme = "README.md" requires-python = ">=3.11" dependencies = [ - "aiosqlite>=0.20.0", - "beautifulsoup4>=4.12.3", - "colorthief>=0.2.1", - "markdownify>=0.13.1", - "numpy>=2.1.2", - "phx-class-registry>=5.1.1", - "pillow>=10.4.0", - "pip>=24.3.1", - "py-dactyl", - "pydantic>=2.9.2", - "red-discordbot>=3.5.14", - "watchdog>=5.0.3", - "websockets>=13.1", + "aiosqlite>=0.20.0", + "beautifulsoup4>=4.12.3", + "colorthief>=0.2.1", + "markdownify>=0.14.1", + "numpy>=2.2.2", + "phx-class-registry>=5.1.1", + "pillow>=10.4.0", + "pip>=25.0", + "py-dactyl", + "pydantic>=2.10.6", + "red-discordbot>=3.5.14", + "watchdog>=6.0.0", + "websockets>=14.2", ] -[project.optional-dependencies] +[dependency-groups] documentation = [ - "mkdocs>=1.6.1", - "mkdocs-git-authors-plugin>=0.9.0", - "mkdocs-git-revision-date-localized-plugin>=1.2.9", - "mkdocs-material[imaging]>=9.5.40", - "mkdocstrings[python]>=0.26.1", - "mkdocs-redirects>=1.2.1", + "mkdocs>=1.6.1", + "mkdocs-git-authors-plugin>=0.9.2", + "mkdocs-git-revision-date-localized-plugin>=1.3.0", + "mkdocs-material[imaging]>=9.5.50", + "mkdocs-redirects>=1.2.2", + "mkdocstrings[python]>=0.27.0", ] [tool.uv] -dev-dependencies = [ - "pylint>=3.3.1", - "ruff>=0.6.9", - "sqlite-web>=0.6.4", -] +dev-dependencies = ["pylint>=3.3.3", "ruff>=0.9.3", "sqlite-web>=0.6.4"] [tool.uv.sources] py-dactyl = { git = "https://github.com/cswimr/pydactyl" } +[tool.basedpyright] +typeCheckingMode = "basic" +reportAttributeAccessIssue = false # disabled because `commands.group.command` is listed as Any / Unknown for some reason + [tool.ruff] # Exclude a variety of commonly ignored directories. exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "site-packages", - "venv", + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", ] # Same as Black. @@ -84,8 +84,32 @@ target-version = "py311" # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. -select = ["F", "W", "E", "C901"] -ignore = ["C901"] +select = [ + "I", + "N", + "F", + "W", + "E", + "G", + "A", + "COM", + "INP", + "T20", + "PLC", + "PLE", + "PLW", + "PLR", + "LOG", + "SLF", + "ERA", + "FIX", + "PERF", + "C4", + "EM", + "RET", + "RSE", +] +ignore = ["PLR0911", "PLR0912", "PLR0915", "PLR2004", "PLR0913", "EM101"] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] diff --git a/seautils/info.json b/seautils/info.json index 2f8fa83..bdd6708 100644 --- a/seautils/info.json +++ b/seautils/info.json @@ -1,10 +1,11 @@ { - "author" : ["cswimr"], - "install_msg" : "Thank you for installing SeaUtils!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", - "name" : "SeaUtils", - "short" : "A collection of useful utilities.", - "description" : "A collection of useful utilities.", - "end_user_data_statement" : "This cog does not store end user data.", + "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/refs/heads/V3/develop/schema/red_cog_repo.schema.json", + "author": ["cswimr"], + "install_msg": "Thank you for installing SeaUtils!\nYou can find the source code of this cog [here](https://coastalcommits.com/cswimr/SeaCogs).", + "name": "SeaUtils", + "short": "A collection of useful utilities.", + "description": "A collection of useful utilities.", + "end_user_data_statement": "This cog does not store end user data.", "hidden": true, "disabled": false, "min_bot_version": "3.5.0", diff --git a/seautils/seautils.py b/seautils/seautils.py index a895c35..126ffd1 100644 --- a/seautils/seautils.py +++ b/seautils/seautils.py @@ -29,18 +29,20 @@ from redbot.core.utils.views import SimpleMenu def md(soup: BeautifulSoup, **options) -> Any | str: return MarkdownConverter(**options).convert_soup(soup=soup) + def format_rfc_text(text: str, number: int) -> str: one: str = re.sub(r"\(\.\/rfc(\d+)", r"(https://www.rfc-editor.org/rfc/rfc\1.html", text) two: str = re.sub(r"\((#(?:section|page)-\d+(?:.\d+)?)\)", f"(https://www.rfc-editor.org/rfc/rfc{number}.html\1)", one) three: str = re.sub(r"\n{3,}", "\n\n", two) return three + class SeaUtils(commands.Cog): """A collection of random utilities.""" __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" - __version__ = "1.0.1" + __version__ = "1.0.2" __documentation__ = "https://seacogs.coastalcommits.com/seautils/" def __init__(self, bot: Red) -> None: @@ -57,7 +59,6 @@ class SeaUtils(commands.Cog): ] return "\n".join(text) - def format_src(self, obj: Any) -> str: """A large portion of this code is repurposed from Zephyrkul's RTFS cog. https://github.com/Zephyrkul/FluffyCogs/blob/master/rtfs/rtfs.py""" @@ -73,9 +74,9 @@ class SeaUtils(commands.Cog): src = obj.function return inspect.getsource(object=src) - @commands.command(aliases=["source", "src", "code", "showsource"]) + @commands.command(aliases=["source", "src", "code", "showsource"]) # type: ignore @commands.is_owner() - async def showcode(self, ctx: commands.Context, *, object: str) -> None: # pylint: disable=redefined-builtin + async def showcode(self, ctx: commands.Context, *, object: str) -> None: # pylint: disable=redefined-builtin # noqa: A002 """Show the code for a particular object.""" try: if object.startswith("/") and (obj := ctx.bot.tree.get_command(object[1:])): @@ -86,11 +87,7 @@ class SeaUtils(commands.Cog): text = self.format_src(obj) else: raise AttributeError - temp_content = cf.pagify( - text=cleanup_code(text), - escape_mass_mentions=True, - page_length = 1977 - ) + temp_content = cf.pagify(text=cleanup_code(text), escape_mass_mentions=True, page_length=1977) content = [] max_i = operator.length_hint(temp_content) i = 1 @@ -105,7 +102,7 @@ class SeaUtils(commands.Cog): else: await ctx.send(content="Object not found!", reference=ctx.message.to_reference(fail_if_not_exists=False)) - @commands.command(name='dig', aliases=['dnslookup', 'nslookup']) + @commands.command(name="dig", aliases=["dnslookup", "nslookup"]) # type: ignore @commands.is_owner() async def dig(self, ctx: commands.Context, name: str, record_type: str | None = None, server: str | None = None, port: int = 53) -> None: """Retrieve DNS information for a domain. @@ -113,13 +110,13 @@ class SeaUtils(commands.Cog): Uses `dig` to perform a DNS query. Will fall back to `nslookup` if `dig` is not installed on the system. `nslookup` does not provide as much information as `dig`, so only the `name` parameter will be used if `nslookup` is used. Will return the A, AAAA, and CNAME records for a domain by default. You can specify a different record type with the `type` parameter.""" - command_opts: list[str | int] = ['dig'] - query_types: list[str] = [record_type] if record_type else ['A', 'AAAA', 'CNAME'] + command_opts: list[str] = ["dig"] + query_types: list[str] = [record_type] if record_type else ["A", "AAAA", "CNAME"] if server: - command_opts.extend(['@', server]) + command_opts.extend(["@", server]) for query_type in query_types: command_opts.extend([name, query_type]) - command_opts.extend(['-p', str(port), '+yaml']) + command_opts.extend(["-p", str(port), "+yaml"]) try: process: Process = await asyncio.create_subprocess_exec(*command_opts, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) @@ -128,22 +125,18 @@ class SeaUtils(commands.Cog): await ctx.maybe_send_embed(message="An error was encountered!\n" + cf.box(text=stderr.decode())) else: data = yaml.safe_load(stdout.decode()) - message_data: dict = data[0]['message'] - response_data: dict = message_data['response_message_data'] + message_data: dict = data[0]["message"] + response_data: dict = message_data["response_message_data"] if ctx.embed_requested(): - embed = Embed( - title="DNS Query Result", - color=await ctx.embed_color(), - timestamp=message_data['response_time'] - ) - embed.add_field(name="Response Address", value=message_data['response_address'], inline=True) - embed.add_field(name="Response Port", value=message_data['response_port'], inline=True) - embed.add_field(name="Query Address", value=message_data['query_address'], inline=True) - embed.add_field(name="Query Port", value=message_data['query_port'], inline=True) - embed.add_field(name="Status", value=response_data['status'], inline=True) - embed.add_field(name="Flags", value=response_data['flags'], inline=True) + embed = Embed(title="DNS Query Result", color=await ctx.embed_color(), timestamp=message_data["response_time"]) + embed.add_field(name="Response Address", value=message_data["response_address"], inline=True) + embed.add_field(name="Response Port", value=message_data["response_port"], inline=True) + embed.add_field(name="Query Address", value=message_data["query_address"], inline=True) + embed.add_field(name="Query Port", value=message_data["query_port"], inline=True) + embed.add_field(name="Status", value=response_data["status"], inline=True) + embed.add_field(name="Flags", value=response_data["flags"], inline=True) - if response_data.get('status') != 'NOERROR': + if response_data.get("status") != "NOERROR": embed.colour = Color.red() embed.description = cf.error("Dig query did not return `NOERROR` status.") @@ -151,19 +144,19 @@ class SeaUtils(commands.Cog): answers = [] authorities = [] for m in data: - response = m['message']['response_message_data'] - if 'QUESTION_SECTION' in response: - for question in response['QUESTION_SECTION']: + response = m["message"]["response_message_data"] + if "QUESTION_SECTION" in response: + for question in response["QUESTION_SECTION"]: if question not in questions: questions.append(question) - if 'ANSWER_SECTION' in response: - for answer in response['ANSWER_SECTION']: + if "ANSWER_SECTION" in response: + for answer in response["ANSWER_SECTION"]: if answer not in answers: answers.append(answer) - if 'AUTHORITY_SECTION' in response: - for authority in response['AUTHORITY_SECTION']: + if "AUTHORITY_SECTION" in response: + for authority in response["AUTHORITY_SECTION"]: if authority not in authorities: authorities.append(authority) @@ -183,26 +176,22 @@ class SeaUtils(commands.Cog): embed.add_field(name="Authority Section", value=f"{cf.box(text=authority_section, lang='prolog')}", inline=False) await ctx.send(embed=embed) else: - await ctx.send(content=cf.box(text=stdout, lang='yaml')) - except (FileNotFoundError): + await ctx.send(content=cf.box(text=str(stdout), lang="yaml")) + except FileNotFoundError: try: - ns_process = await asyncio.create_subprocess_exec('nslookup', name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + ns_process = await asyncio.create_subprocess_exec("nslookup", name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) ns_stdout, ns_stderr = await ns_process.communicate() if ns_stderr: await ctx.maybe_send_embed(message="An error was encountered!\n" + cf.box(text=ns_stderr.decode())) else: warning = cf.warning("`dig` is not installed! Defaulting to `nslookup`.\nThis command provides more information when `dig` is installed on the system.\n") if await ctx.embed_requested(): - embed = Embed( - title="DNS Query Result", - color=await ctx.embed_color(), - timestamp=ctx.message.created_at - ) + embed = Embed(title="DNS Query Result", color=await ctx.embed_color(), timestamp=ctx.message.created_at) embed.description = warning + cf.box(text=ns_stdout.decode()) await ctx.send(embed=embed) else: - await ctx.send(content = warning + cf.box(text=ns_stdout.decode())) - except (FileNotFoundError): + await ctx.send(content=warning + cf.box(text=ns_stdout.decode())) + except FileNotFoundError: await ctx.maybe_send_embed(message=cf.error("Neither `dig` nor `nslookup` are installed on the system. Unable to resolve DNS query.")) @commands.command() @@ -210,45 +199,34 @@ class SeaUtils(commands.Cog): """Retrieve the text of an RFC document. This command uses the [RFC Editor website](https://www.rfc-editor.org/) to fetch the text of an RFC document. - A [Request for Comments (RFC)](https://en.wikipedia.org/wiki/Request_for_Comments) is a publication in a series from the principal technical development and standards-setting bodies for the [Internet](https://en.wikipedia.org/wiki/Internet), most prominently the [Internet Engineering Task Force](https://en.wikipedia.org/wiki/Internet_Engineering_Task_Force). An RFC is authored by individuals or groups of engineers and [computer scientists](https://en.wikipedia.org/wiki/Computer_scientist) in the form of a [memorandum](https://en.wikipedia.org/wiki/Memorandum) describing methods, behaviors, research, or innovations applicable to the working of the Internet and Internet-connected systems. It is submitted either for [peer review](https://en.wikipedia.org/wiki/Peer_review) or to convey new concepts, information, or, occasionally, engineering humor.""" # noqa: E501 + A [Request for Comments (RFC)](https://en.wikipedia.org/wiki/Request_for_Comments) is a publication in a series from the principal technical development and standards-setting bodies for the [Internet](https://en.wikipedia.org/wiki/Internet), most prominently the [Internet Engineering Task Force](https://en.wikipedia.org/wiki/Internet_Engineering_Task_Force). An RFC is authored by individuals or groups of engineers and [computer scientists](https://en.wikipedia.org/wiki/Computer_scientist) in the form of a [memorandum](https://en.wikipedia.org/wiki/Memorandum) describing methods, behaviors, research, or innovations applicable to the working of the Internet and Internet-connected systems. It is submitted either for [peer review](https://en.wikipedia.org/wiki/Peer_review) or to convey new concepts, information, or, occasionally, engineering humor.""" # noqa: E501 url = f"https://www.rfc-editor.org/rfc/rfc{number}.html" datatracker_url = f"https://datatracker.ietf.org/doc/rfc{number}" async with aiohttp.ClientSession() as session: async with session.get(url=url) as response: if response.status == 200: html = await response.text() - soup = BeautifulSoup(html, 'html.parser') - pre_tags = soup.find_all('pre') - content: list[Embed | str] = [] + soup = BeautifulSoup(html, "html.parser") + pre_tags = soup.find_all("pre") + content: list[str | Embed] = [] for pre_tag in pre_tags: text = format_rfc_text(md(pre_tag), number) if len(text) > 4096: pagified_text = cf.pagify(text, delims=["\n\n"], page_length=4096) for page in pagified_text: if await ctx.embed_requested(): - embed = Embed( - title=f"RFC Document {number}", - url=datatracker_url, - description=page, - color=await ctx.embed_color() - ) + embed = Embed(title=f"RFC Document {number}", url=datatracker_url, description=page, color=await ctx.embed_color()) content.append(embed) else: content.append(page) + elif await ctx.embed_requested(): + embed = Embed(title=f"RFC Document {number}", url=datatracker_url, description=text, color=await ctx.embed_color()) + content.append(embed) else: - if await ctx.embed_requested(): - embed = Embed( - title=f"RFC Document {number}", - url=datatracker_url, - description=text, - color=await ctx.embed_color() - ) - content.append(embed) - else: - content.append(text) + content.append(text) if await ctx.embed_requested(): for embed in content: embed.set_footer(text=f"Page {content.index(embed) + 1}/{len(content)}") - await SimpleMenu(pages=content, disable_after_timeout=True, timeout=300).start(ctx) + await SimpleMenu(pages=content, disable_after_timeout=True, timeout=300).start(ctx) # type: ignore else: await ctx.maybe_send_embed(message=cf.error(f"An error occurred while fetching RFC {number}. Status code: {response.status}.")) diff --git a/uv.lock b/uv.lock index a39fa2f..9cfc50a 100644 --- a/uv.lock +++ b/uv.lock @@ -136,11 +136,11 @@ wheels = [ [[package]] name = "astroid" -version = "3.3.5" +version = "3.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/1e/326fb1d3d83a3bb77c9f9be29d31f2901e35acb94b0605c3f2e5085047f9/astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d", size = 397229 } +sdist = { url = "https://files.pythonhosted.org/packages/80/c5/5c83c48bbf547f3dd8b587529db7cf5a265a3368b33e85e76af8ff6061d3/astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b", size = 398196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/30/624365383fa4a40329c0f0bbbc151abc4a64e30dfc110fc8f6e2afcd02bb/astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8", size = 274586 }, + { url = "https://files.pythonhosted.org/packages/07/28/0bc8a17d6cd4cc3c79ae41b7105a2b9a327c110e5ddd37a8a27b29a5c8a2/astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c", size = 275153 }, ] [[package]] @@ -658,15 +658,15 @@ wheels = [ [[package]] name = "markdownify" -version = "0.13.1" +version = "0.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/5a/bd1b685ee9efbfb0b22774a30188dfb4048c64e8a6c80a65a7f207af4ea1/markdownify-0.13.1.tar.gz", hash = "sha256:ab257f9e6bd4075118828a28c9d02f8a4bfeb7421f558834aa79b2dfeb32a098", size = 13609 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/75/483a4bcca436fe88d02dc7686c372631d833848951b368700bdc0c770bb7/markdownify-0.14.1.tar.gz", hash = "sha256:a62a7a216947ed0b8dafb95b99b2ef4a0edd1e18d5653c656f68f03db2bfb2f1", size = 14332 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e9/6e2757a670b8c48bc48eff1c20cb9d71f1476e844038bdbdb76f17e6a12b/markdownify-0.13.1-py3-none-any.whl", hash = "sha256:1d181d43d20902bcc69d7be85b5316ed174d0dda72ff56e14ae4c95a4a407d22", size = 10800 }, + { url = "https://files.pythonhosted.org/packages/65/0b/74cec93a7b05edf4fc3ea1c899fe8a37f041d7b9d303c75abf7a162924e0/markdownify-0.14.1-py3-none-any.whl", hash = "sha256:4c46a6c0c12c6005ddcd49b45a5a890398b002ef51380cd319db62df5e09bc2a", size = 11530 }, ] [[package]] @@ -770,16 +770,16 @@ wheels = [ [[package]] name = "mkdocs-autorefs" -version = "1.2.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262 } +sdist = { url = "https://files.pythonhosted.org/packages/52/f4/77e3cf5e7ba54dca168bc718688127844721982ae88b08684669c5b5752d/mkdocs_autorefs-1.3.1.tar.gz", hash = "sha256:a6d30cbcccae336d622a66c2418a3c92a8196b69782774529ad441abb23c0902", size = 2056416 } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522 }, + { url = "https://files.pythonhosted.org/packages/db/19/f20edc082c1de2987dbaf30fcc514ed7a908d465a15aba7cba595c3b245a/mkdocs_autorefs-1.3.1-py3-none-any.whl", hash = "sha256:18c504ae4d3ee7f344369bb26cb31d4105569ee252aab7d75ec2734c2c8b0474", size = 2837887 }, ] [[package]] @@ -798,19 +798,19 @@ wheels = [ [[package]] name = "mkdocs-git-authors-plugin" -version = "0.9.0" +version = "0.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/03/99e18d62964d268eb9a866f42c9d53b43cde903a7fb436da85e396945a02/mkdocs_git_authors_plugin-0.9.0.tar.gz", hash = "sha256:6161f63b87064481a48d9ad01c23e43c3e758930c3a9cc167fe482909ceb9eac", size = 20268 } +sdist = { url = "https://files.pythonhosted.org/packages/80/ef/09ab7178d580e342cb3ba279c48eaf3abf55795a2ae6e5426fe2c725143c/mkdocs_git_authors_plugin-0.9.2.tar.gz", hash = "sha256:77f97c321e08a8757beb866293eb257070b11cd5a080976bc6696b249cbade4f", size = 21403 } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/ad/a6e0ce34a1d9abe35844cdc3a64028d3df7bfe24b143021f7fcaa26adfdd/mkdocs_git_authors_plugin-0.9.0-py3-none-any.whl", hash = "sha256:380730a05eeb947a7e84be05fdb1c5ae2a7bc70fd9f6eda941f187c87ae37052", size = 19204 }, + { url = "https://files.pythonhosted.org/packages/48/08/57d0fea1cc30096fcc94ec9cd4ccdee625be89fd710626f78d90fc13738e/mkdocs_git_authors_plugin-0.9.2-py3-none-any.whl", hash = "sha256:f6cefc4dc832865d26f7f9f944c0a8c7dc852742d79320f3800e0d97814e2a84", size = 20332 }, ] [[package]] name = "mkdocs-git-revision-date-localized-plugin" -version = "1.2.9" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -818,14 +818,14 @@ dependencies = [ { name = "mkdocs" }, { name = "pytz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/79/56c755035c893af33c3ba29c5100835d10cd98b4b6943f8d1c22a7d56936/mkdocs_git_revision_date_localized_plugin-1.2.9.tar.gz", hash = "sha256:df9a50873fba3a42ce9123885f8c53d589e90ef6c2443fe3280ef1e8d33c8f65", size = 384360 } +sdist = { url = "https://files.pythonhosted.org/packages/73/85/6dc9d4eca486ed5734a05f7fd5c612a8e60a35e65610dad6aa9c58118c3f/mkdocs_git_revision_date_localized_plugin-1.3.0.tar.gz", hash = "sha256:439e2f14582204050a664c258861c325064d97cdc848c541e48bb034a6c4d0cb", size = 384797 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/05/0edbbd3a0be3033c44d5cd9f1ac7646da2e7e3911513cc56a25aac9266a4/mkdocs_git_revision_date_localized_plugin-1.2.9-py3-none-any.whl", hash = "sha256:dea5c8067c23df30275702a1708885500fadf0abfb595b60e698bffc79c7a423", size = 22475 }, + { url = "https://files.pythonhosted.org/packages/67/e5/ffeb92db53af8c3aa2d92e21a3cf6b5f83eee7e03b9cf9234ef6b30230d5/mkdocs_git_revision_date_localized_plugin-1.3.0-py3-none-any.whl", hash = "sha256:c99377ee119372d57a9e47cff4e68f04cce634a74831c06bc89b33e456e840a1", size = 22549 }, ] [[package]] name = "mkdocs-material" -version = "9.5.40" +version = "9.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -840,9 +840,9 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/2b/6f9e0b9573a4acfa15834a30eca48ad578fe6ab46afa072df5ff05103a86/mkdocs_material-9.5.40.tar.gz", hash = "sha256:b69d70e667ec51fc41f65e006a3184dd00d95b2439d982cb1586e4c018943156", size = 3963129 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/80/4efbd3df76c6c1ec27130b43662612f9033adc5a4166f1df2acb8dd6fb1b/mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867", size = 3942628 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ad/f8039114a23cfb02213f133a1dc8865522128a0b2cb251a7e717de8aa979/mkdocs_material-9.5.40-py3-none-any.whl", hash = "sha256:8e7a16ada34e79a7b6459ff2602584222f522c738b6a023d1bea853d5049da6f", size = 8670419 }, + { url = "https://files.pythonhosted.org/packages/5b/a5/f3c0e86c1d28fe04f1b724700ff3dd8b3647c89df03a8e10c4bc6b4db1b8/mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f", size = 8688727 }, ] [package.optional-dependencies] @@ -862,33 +862,32 @@ wheels = [ [[package]] name = "mkdocs-redirects" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/6a/50edd7ad78042b25c379aac7e8fa9cc34c6f55e3d2c03eb28814a9446617/mkdocs-redirects-1.2.1.tar.gz", hash = "sha256:9420066d70e2a6bb357adf86e67023dcdca1857f97f07c7fe450f8f1fb42f861", size = 6653 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/9d/93a881fc5a23c50a4dd4a41dfd3d2a8403aa1dac52370ef43b7b336577a0/mkdocs_redirects-1.2.1-py3-none-any.whl", hash = "sha256:497089f9e0219e7389304cffefccdfa1cac5ff9509f2cb706f4c9b221726dffb", size = 6024 }, + { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142 }, ] [[package]] name = "mkdocstrings" -version = "0.26.1" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, - { name = "platformdirs" }, + { name = "mkdocs-get-deps" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677 } +sdist = { url = "https://files.pythonhosted.org/packages/82/6a/3980e05d7522423dc4ca547771d16d399fc3f4266df652f624f4f4dd7890/mkdocstrings-0.28.1.tar.gz", hash = "sha256:fb64576906771b7701e8e962fd90073650ff689e95eb86e86751a66d65ab4489", size = 4551690 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643 }, + { url = "https://files.pythonhosted.org/packages/6f/5d/8580b426396d8cbbe98df364ef891487c4942e36356d56bb5a6dd91f51a9/mkdocstrings-0.28.1-py3-none-any.whl", hash = "sha256:a5878ae5cd1e26f491ff084c1f9ab995687d52d39a5c558e9b7023d0e4e0b740", size = 6426938 }, ] [package.optional-dependencies] @@ -966,48 +965,50 @@ wheels = [ [[package]] name = "numpy" -version = "2.1.2" +version = "2.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/d1/8a730ea07f4a37d94f9172f4ce1d81064b7a64766b460378be278952de75/numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c", size = 18878063 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/9c/9a6ec3ae89cd0648d419781284308f2956d2a61d932b5ac9682c956a171b/numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe", size = 21154845 }, - { url = "https://files.pythonhosted.org/packages/02/69/9f05c4ecc75fabf297b17743996371b4c3dfc4d92e15c5c38d8bb3db8d74/numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1", size = 13789409 }, - { url = "https://files.pythonhosted.org/packages/34/4e/f95c99217bf77bbfaaf660d693c10bd0dc03b6032d19316d316088c9e479/numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f", size = 5352097 }, - { url = "https://files.pythonhosted.org/packages/06/13/f5d87a497c16658e9af8920449b0b5692b469586b8231340c672962071c5/numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4", size = 6891195 }, - { url = "https://files.pythonhosted.org/packages/6c/89/691ac07429ac061b344d5e37fa8e94be51a6017734aea15f2d9d7c6d119a/numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a", size = 13895153 }, - { url = "https://files.pythonhosted.org/packages/23/69/538317f0d925095537745f12aced33be1570bbdc4acde49b33748669af96/numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1", size = 16338306 }, - { url = "https://files.pythonhosted.org/packages/af/03/863fe7062c2106d3c151f7df9353f2ae2237c1dd6900f127a3eb1f24cb1b/numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2", size = 16710893 }, - { url = "https://files.pythonhosted.org/packages/70/77/0ad9efe25482009873f9660d29a40a8c41a6f0e8b541195e3c95c70684c5/numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146", size = 14398048 }, - { url = "https://files.pythonhosted.org/packages/3e/0f/e785fe75544db9f2b0bb1c181e13ceff349ce49753d807fd9672916aa06d/numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c", size = 6533458 }, - { url = "https://files.pythonhosted.org/packages/d4/96/450054662295125af861d48d2c4bc081dadcf1974a879b2104613157aa62/numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9", size = 12870896 }, - { url = "https://files.pythonhosted.org/packages/a0/7d/554a6838f37f3ada5a55f25173c619d556ae98092a6e01afb6e710501d70/numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b", size = 20848077 }, - { url = "https://files.pythonhosted.org/packages/b0/29/cb48a402ea879e645b16218718f3f7d9588a77d674a9dcf22e4c43487636/numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db", size = 13493242 }, - { url = "https://files.pythonhosted.org/packages/56/44/f899b0581766c230da42f751b7b8896d096640b19b312164c267e48d36cb/numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1", size = 5089219 }, - { url = "https://files.pythonhosted.org/packages/79/8f/b987070d45161a7a4504afc67ed38544ed2c0ed5576263599a0402204a9c/numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426", size = 6620167 }, - { url = "https://files.pythonhosted.org/packages/c4/a7/af3329fda3c3ec31d9b650e42bbcd3422fc62a765cbb1405fde4177a0996/numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0", size = 13604905 }, - { url = "https://files.pythonhosted.org/packages/9b/b4/e3c7e6fab0f77fff6194afa173d1f2342073d91b1d3b4b30b17c3fb4407a/numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df", size = 16041825 }, - { url = "https://files.pythonhosted.org/packages/e9/50/6828e66a78aa03147c111f84d55f33ce2dde547cb578d6744a3b06a0124b/numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366", size = 16409541 }, - { url = "https://files.pythonhosted.org/packages/bf/72/66af7916d9c3c6dbfbc8acdd4930c65461e1953374a2bc43d00f948f004a/numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142", size = 14081134 }, - { url = "https://files.pythonhosted.org/packages/dc/5a/59a67d84f33fe00ae74f0b5b69dd4f93a586a4aba7f7e19b54b2133db038/numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550", size = 6237784 }, - { url = "https://files.pythonhosted.org/packages/4c/79/73735a6a5dad6059c085f240a4e74c9270feccd2bc66e4d31b5ca01d329c/numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e", size = 12568254 }, - { url = "https://files.pythonhosted.org/packages/16/72/716fa1dbe92395a9a623d5049203ff8ddb0cfce65b9df9117c3696ccc011/numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d", size = 20834690 }, - { url = "https://files.pythonhosted.org/packages/1e/fb/3e85a39511586053b5c6a59a643879e376fae22230ebfef9cfabb0e032e2/numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf", size = 13507474 }, - { url = "https://files.pythonhosted.org/packages/35/eb/5677556d9ba13436dab51e129f98d4829d95cd1b6bd0e199c14485a4bdb9/numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e", size = 5074742 }, - { url = "https://files.pythonhosted.org/packages/3e/c5/6c5ef5ba41b65a7e51bed50dbf3e1483eb578055633dd013e811a28e96a1/numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3", size = 6606787 }, - { url = "https://files.pythonhosted.org/packages/08/ac/f2f29dd4fd325b379c7dc932a0ebab22f0e031dbe80b2f6019b291a3a544/numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8", size = 13601333 }, - { url = "https://files.pythonhosted.org/packages/44/26/63f5f4e5089654dfb858f4892215ed968cd1a68e6f4a83f9961f84f855cb/numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a", size = 16038090 }, - { url = "https://files.pythonhosted.org/packages/1d/21/015e0594de9c3a8d5edd24943d2bd23f102ec71aec026083f822f86497e2/numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98", size = 16410865 }, - { url = "https://files.pythonhosted.org/packages/df/01/c1bcf9e6025d79077fbf3f3ee503b50aa7bfabfcd8f4b54f5829f4c00f3f/numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe", size = 14078077 }, - { url = "https://files.pythonhosted.org/packages/ba/06/db9d127d63bd11591770ba9f3d960f8041e0f895184b9351d4b1b5b56983/numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a", size = 6234904 }, - { url = "https://files.pythonhosted.org/packages/a9/96/9f61f8f95b6e0ea0aa08633b704c75d1882bdcb331bdf8bfd63263b25b00/numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445", size = 12561910 }, - { url = "https://files.pythonhosted.org/packages/36/b8/033f627821784a48e8f75c218033471eebbaacdd933f8979c79637a1b44b/numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5", size = 20857719 }, - { url = "https://files.pythonhosted.org/packages/96/46/af5726fde5b74ed83f2f17a73386d399319b7ed4d51279fb23b721d0816d/numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0", size = 13518826 }, - { url = "https://files.pythonhosted.org/packages/db/6e/8ce677edf36da1c4dae80afe5529f47690697eb55b4864673af260ccea7b/numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17", size = 5115036 }, - { url = "https://files.pythonhosted.org/packages/6a/ba/3cce44fb1b8438042c11847048812a776f75ee0e7070179c22e4cfbf420c/numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6", size = 6628641 }, - { url = "https://files.pythonhosted.org/packages/59/c8/e722998720ccbd35ffbcf1d1b8ed0aa2304af88d3f1c38e06ebf983599b3/numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8", size = 13574803 }, - { url = "https://files.pythonhosted.org/packages/7c/8e/fc1fdd83a55476765329ac2913321c4aed5b082a7915095628c4ca30ea72/numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35", size = 16021174 }, - { url = "https://files.pythonhosted.org/packages/2a/b6/a790742aa88067adb4bd6c89a946778c1417d4deaeafce3ca928f26d4c52/numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62", size = 16400117 }, - { url = "https://files.pythonhosted.org/packages/48/6f/129e3c17e3befe7fefdeaa6890f4c4df3f3cf0831aa053802c3862da67aa/numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a", size = 14066202 }, + { url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 }, + { url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 }, + { url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 }, + { url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 }, + { url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 }, + { url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 }, + { url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 }, + { url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 }, + { url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 }, + { url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 }, + { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 }, + { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 }, + { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 }, + { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 }, + { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 }, + { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 }, + { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 }, + { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 }, + { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 }, + { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 }, + { url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 }, + { url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 }, + { url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 }, + { url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 }, + { url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 }, + { url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 }, + { url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 }, + { url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 }, + { url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 }, + { url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 }, + { url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 }, + { url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 }, + { url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 }, + { url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 }, + { url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 }, + { url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 }, + { url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 }, + { url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 }, + { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 }, ] [[package]] @@ -1138,11 +1139,11 @@ wheels = [ [[package]] name = "pip" -version = "24.3.1" +version = "25.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, + { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526 }, ] [[package]] @@ -1245,63 +1246,69 @@ wheels = [ [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, ] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, - { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, - { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, - { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, - { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, - { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, - { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, - { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, - { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, - { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, - { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, - { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, - { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, - { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, - { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, - { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, - { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, - { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, - { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, - { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, - { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, - { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, - { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, - { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, - { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, - { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, - { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, - { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, - { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, - { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, - { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, - { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, - { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, - { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, ] [[package]] @@ -1315,7 +1322,7 @@ wheels = [ [[package]] name = "pylint" -version = "3.3.1" +version = "3.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, @@ -1326,9 +1333,9 @@ dependencies = [ { name = "platformdirs" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/3a/13e90e29777e695d90f422cf4fadb81c999e4755a9089838561bd0590cac/pylint-3.3.1.tar.gz", hash = "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e", size = 1516703 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/50be49afc91469f832c4bf12318ab4abe56ee9aa3700a89aad5359ad195f/pylint-3.3.4.tar.gz", hash = "sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce", size = 1518905 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/11/4a3f814eee14593f3cfcf7046bc765bf1646d5c88132c08c45310fc7d85f/pylint-3.3.1-py3-none-any.whl", hash = "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9", size = 521768 }, + { url = "https://files.pythonhosted.org/packages/0d/8b/eef15df5f4e7aa393de31feb96ca9a3d6639669bd59d589d0685d5ef4e62/pylint-3.3.4-py3-none-any.whl", hash = "sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018", size = 522280 }, ] [[package]] @@ -1619,27 +1626,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.6.9" +version = "0.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/0d/6148a48dab5662ca1d5a93b7c0d13c03abd3cc7e2f35db08410e47cef15d/ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", size = 3095355 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/8f/f7a0a0ef1818662efb32ed6df16078c95da7a0a3248d64c2410c1e27799f/ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", size = 10440526 }, - { url = "https://files.pythonhosted.org/packages/8b/69/b179a5faf936a9e2ab45bb412a668e4661eded964ccfa19d533f29463ef6/ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", size = 10034612 }, - { url = "https://files.pythonhosted.org/packages/c7/ef/fd1b4be979c579d191eeac37b5cfc0ec906de72c8bcd8595e2c81bb700c1/ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", size = 9706197 }, - { url = "https://files.pythonhosted.org/packages/29/61/b376d775deb5851cb48d893c568b511a6d3625ef2c129ad5698b64fb523c/ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", size = 10751855 }, - { url = "https://files.pythonhosted.org/packages/13/d7/def9e5f446d75b9a9c19b24231a3a658c075d79163b08582e56fa5dcfa38/ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", size = 10200889 }, - { url = "https://files.pythonhosted.org/packages/6c/d6/7f34160818bcb6e84ce293a5966cba368d9112ff0289b273fbb689046047/ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", size = 11038678 }, - { url = "https://files.pythonhosted.org/packages/13/34/a40ff8ae62fb1b26fb8e6fa7e64bc0e0a834b47317880de22edd6bfb54fb/ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", size = 11808682 }, - { url = "https://files.pythonhosted.org/packages/2e/6d/25a4386ae4009fc798bd10ba48c942d1b0b3e459b5403028f1214b6dd161/ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", size = 11330446 }, - { url = "https://files.pythonhosted.org/packages/f7/f6/bdf891a9200d692c94ebcd06ae5a2fa5894e522f2c66c2a12dd5d8cb2654/ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", size = 12483048 }, - { url = "https://files.pythonhosted.org/packages/a7/86/96f4252f41840e325b3fa6c48297e661abb9f564bd7dcc0572398c8daa42/ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", size = 10936855 }, - { url = "https://files.pythonhosted.org/packages/45/87/801a52d26c8dbf73424238e9908b9ceac430d903c8ef35eab1b44fcfa2bd/ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", size = 10713007 }, - { url = "https://files.pythonhosted.org/packages/be/27/6f7161d90320a389695e32b6ebdbfbedde28ccbf52451e4b723d7ce744ad/ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", size = 10274594 }, - { url = "https://files.pythonhosted.org/packages/00/52/dc311775e7b5f5b19831563cb1572ecce63e62681bccc609867711fae317/ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", size = 10608024 }, - { url = "https://files.pythonhosted.org/packages/98/b6/be0a1ddcbac65a30c985cf7224c4fce786ba2c51e7efeb5178fe410ed3cf/ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", size = 10982085 }, - { url = "https://files.pythonhosted.org/packages/bb/a4/c84bc13d0b573cf7bb7d17b16d6d29f84267c92d79b2f478d4ce322e8e72/ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d", size = 8522088 }, - { url = "https://files.pythonhosted.org/packages/74/be/fc352bd8ca40daae8740b54c1c3e905a7efe470d420a268cd62150248c91/ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", size = 9359275 }, - { url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879 }, + { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, + { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, + { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, + { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, + { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, + { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, + { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, + { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, + { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, + { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, + { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, + { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, + { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, + { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, ] [[package]] @@ -1671,7 +1678,12 @@ dependencies = [ { name = "websockets" }, ] -[package.optional-dependencies] +[package.dev-dependencies] +dev = [ + { name = "pylint" }, + { name = "ruff" }, + { name = "sqlite-web" }, +] documentation = [ { name = "mkdocs" }, { name = "mkdocs-git-authors-plugin" }, @@ -1681,42 +1693,37 @@ documentation = [ { name = "mkdocstrings", extra = ["python"] }, ] -[package.dev-dependencies] -dev = [ - { name = "pylint" }, - { name = "ruff" }, - { name = "sqlite-web" }, -] - [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.20.0" }, { name = "beautifulsoup4", specifier = ">=4.12.3" }, { name = "colorthief", specifier = ">=0.2.1" }, - { name = "markdownify", specifier = ">=0.13.1" }, - { name = "mkdocs", marker = "extra == 'documentation'", specifier = ">=1.6.1" }, - { name = "mkdocs-git-authors-plugin", marker = "extra == 'documentation'", specifier = ">=0.9.0" }, - { name = "mkdocs-git-revision-date-localized-plugin", marker = "extra == 'documentation'", specifier = ">=1.2.9" }, - { name = "mkdocs-material", extras = ["imaging"], marker = "extra == 'documentation'", specifier = ">=9.5.40" }, - { name = "mkdocs-redirects", marker = "extra == 'documentation'", specifier = ">=1.2.1" }, - { name = "mkdocstrings", extras = ["python"], marker = "extra == 'documentation'", specifier = ">=0.26.1" }, - { name = "numpy", specifier = ">=2.1.2" }, + { name = "markdownify", specifier = ">=0.14.1" }, + { name = "numpy", specifier = ">=2.2.2" }, { name = "phx-class-registry", specifier = ">=5.1.1" }, { name = "pillow", specifier = ">=10.4.0" }, - { name = "pip", specifier = ">=24.3.1" }, + { name = "pip", specifier = ">=25.0" }, { name = "py-dactyl", git = "https://github.com/cswimr/pydactyl" }, - { name = "pydantic", specifier = ">=2.9.2" }, + { name = "pydantic", specifier = ">=2.10.6" }, { name = "red-discordbot", specifier = ">=3.5.14" }, - { name = "watchdog", specifier = ">=5.0.3" }, - { name = "websockets", specifier = ">=13.1" }, + { name = "watchdog", specifier = ">=6.0.0" }, + { name = "websockets", specifier = ">=14.2" }, ] [package.metadata.requires-dev] dev = [ - { name = "pylint", specifier = ">=3.3.1" }, - { name = "ruff", specifier = ">=0.6.9" }, + { name = "pylint", specifier = ">=3.3.3" }, + { name = "ruff", specifier = ">=0.9.3" }, { name = "sqlite-web", specifier = ">=0.6.4" }, ] +documentation = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.2" }, + { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.3.0" }, + { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5.50" }, + { name = "mkdocs-redirects", specifier = ">=1.2.2" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0" }, +] [[package]] name = "six" @@ -1823,29 +1830,29 @@ wheels = [ [[package]] name = "watchdog" -version = "5.0.3" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/48/a86139aaeab2db0a2482676f64798d8ac4d2dbb457523f50ab37bf02ce2c/watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176", size = 129556 } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/34/946f08602f8b8e6af45bc725e4a8013975a34883ab5570bd0d827a4c9829/watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818", size = 96650 }, - { url = "https://files.pythonhosted.org/packages/96/2b/b84e35d49e8b0bad77e5d086fc1e2c6c833bbfe74d53144cfe8b26117eff/watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490", size = 88653 }, - { url = "https://files.pythonhosted.org/packages/d5/3f/41b5d77c10f450b79921c17b7d0b416616048867bfe63acaa072a619a0cb/watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e", size = 89286 }, - { url = "https://files.pythonhosted.org/packages/1c/9b/8b206a928c188fdeb7b12e1c795199534cd44bdef223b8470129016009dd/watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8", size = 96739 }, - { url = "https://files.pythonhosted.org/packages/e1/26/129ca9cd0f8016672f37000010c2fedc0b86816e894ebdc0af9bb04a6439/watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926", size = 88708 }, - { url = "https://files.pythonhosted.org/packages/8f/b3/5e10ec32f0c429cdb55b1369066d6e83faf9985b3a53a4e37bb5c5e29aa0/watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e", size = 89309 }, - { url = "https://files.pythonhosted.org/packages/54/c4/49af4ab00bcfb688e9962eace2edda07a2cf89b9699ea536da48e8585cff/watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7", size = 96740 }, - { url = "https://files.pythonhosted.org/packages/96/a4/b24de77cc9ae424c1687c9d4fb15aa560d7d7b28ba559aca72f781d0202b/watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906", size = 88711 }, - { url = "https://files.pythonhosted.org/packages/a4/71/3f2e9fe8403386b99d788868955b3a790f7a09721501a7e1eb58f514ffaa/watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1", size = 89319 }, - { url = "https://files.pythonhosted.org/packages/60/33/7cb71c9df9a77b6927ee5f48d25e1de5562ce0fa7e0c56dcf2b0472e64a2/watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91", size = 79335 }, - { url = "https://files.pythonhosted.org/packages/f6/91/320bc1496cf951a3cf93a7ffd18a581f0792c304be963d943e0e608c2919/watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c", size = 79334 }, - { url = "https://files.pythonhosted.org/packages/8b/2c/567c5e042ed667d3544c43d48a65cf853450a2d2a9089d9523a65f195e94/watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c", size = 79333 }, - { url = "https://files.pythonhosted.org/packages/c3/f0/64059fe162ef3274662e67bbdea6c45b3cd53e846d5bd1365fcdc3dc1d15/watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221", size = 79334 }, - { url = "https://files.pythonhosted.org/packages/f6/d9/19b7d02965be2801e2d0f6f4bde23e4ae172620071b65430fa0c2f8441ac/watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05", size = 79333 }, - { url = "https://files.pythonhosted.org/packages/cb/a1/5393ac6d0b095d3a44946b09258e9b5f22cb2fb67bcfa419dd868478826c/watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97", size = 79332 }, - { url = "https://files.pythonhosted.org/packages/a0/58/edec25190b6403caf4426dd418234f2358a106634b7d6aa4aec6939b104f/watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7", size = 79334 }, - { url = "https://files.pythonhosted.org/packages/97/69/cfb2d17ba8aabc73be2e2d03c8c319b1f32053a02c4b571852983aa24ff2/watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49", size = 79320 }, - { url = "https://files.pythonhosted.org/packages/91/b4/2b5b59358dadfa2c8676322f955b6c22cde4937602f40490e2f7403e548e/watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9", size = 79325 }, - { url = "https://files.pythonhosted.org/packages/38/b8/0aa69337651b3005f161f7f494e59188a1d8d94171666900d26d29d10f69/watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45", size = 79324 }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] [[package]] @@ -1859,44 +1866,44 @@ wheels = [ [[package]] name = "websockets" -version = "13.1" +version = "15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/8bc4d15af7ff30f7ba34f9a172063bfcee9f5001d7cef04bee800a658f33/websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab", size = 175574 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813 }, - { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469 }, - { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717 }, - { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379 }, - { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376 }, - { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753 }, - { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051 }, - { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489 }, - { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438 }, - { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710 }, - { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137 }, - { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821 }, - { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480 }, - { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715 }, - { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647 }, - { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592 }, - { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012 }, - { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311 }, - { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692 }, - { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, - { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, - { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, - { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, - { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, - { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, - { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, - { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, - { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, - { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, - { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, - { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, - { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, - { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, - { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 }, + { url = "https://files.pythonhosted.org/packages/ee/16/81a7403c8c0a33383de647e89c07824ea6a654e3877d6ff402cbae298cb8/websockets-15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd24c4d256558429aeeb8d6c24ebad4e982ac52c50bc3670ae8646c181263965", size = 174702 }, + { url = "https://files.pythonhosted.org/packages/ef/40/4629202386a3bf1195db9fe41baeb1d6dfd8d72e651d9592d81dae7fdc7c/websockets-15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f83eca8cbfd168e424dfa3b3b5c955d6c281e8fc09feb9d870886ff8d03683c7", size = 172359 }, + { url = "https://files.pythonhosted.org/packages/7b/33/dfb650e822bc7912d8c542c452497867af91dec81e7b5bf96aca5b419d58/websockets-15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4095a1f2093002c2208becf6f9a178b336b7572512ee0a1179731acb7788e8ad", size = 172604 }, + { url = "https://files.pythonhosted.org/packages/2e/52/666743114513fcffd43ee5df261a1eb5d41f8e9861b7a190b730732c19ba/websockets-15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb915101dfbf318486364ce85662bb7b020840f68138014972c08331458d41f3", size = 182145 }, + { url = "https://files.pythonhosted.org/packages/9c/63/5273f146b13aa4a057a95ab0855d9990f3a1ced63693f4365135d1abfacc/websockets-15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45d464622314973d78f364689d5dbb9144e559f93dca11b11af3f2480b5034e1", size = 181152 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/075697f3f97de7c26b73ae96d952e13fa36393e0db3f028540b28954e0a9/websockets-15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace960769d60037ca9625b4c578a6f28a14301bd2a1ff13bb00e824ac9f73e55", size = 181523 }, + { url = "https://files.pythonhosted.org/packages/25/87/06d091bbcbe01903bed3dad3bb4a1a3c516f61e611ec31fffb28abe4974b/websockets-15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd4b1015d2f60dfe539ee6c95bc968d5d5fad92ab01bb5501a77393da4f596", size = 181791 }, + { url = "https://files.pythonhosted.org/packages/77/08/5063b6cc1b2aa1fba2ee3b578b777db22fde7145f121d07fd878811e983b/websockets-15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f7290295794b5dec470867c7baa4a14182b9732603fd0caf2a5bf1dc3ccabf3", size = 181231 }, + { url = "https://files.pythonhosted.org/packages/86/ff/af23084df0a7405bb2add12add8c17d6192a8de9480f1b90d12352ba2b7d/websockets-15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3abd670ca7ce230d5a624fd3d55e055215d8d9b723adee0a348352f5d8d12ff4", size = 181191 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b2bdfcf49201dee0b899edc6a814755763ec03d74f2714923d38453a9e8d/websockets-15.0-cp311-cp311-win32.whl", hash = "sha256:110a847085246ab8d4d119632145224d6b49e406c64f1bbeed45c6f05097b680", size = 175666 }, + { url = "https://files.pythonhosted.org/packages/8d/7b/444edcd5365538c226b631897975a65bbf5ccf27c77102e17d8f12a306ea/websockets-15.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7bbbe2cd6ed80aceef2a14e9f1c1b61683194c216472ed5ff33b700e784e37", size = 176105 }, + { url = "https://files.pythonhosted.org/packages/22/1e/92c4547d7b2a93f848aedaf37e9054111bc00dc11bff4385ca3f80dbb412/websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f", size = 174709 }, + { url = "https://files.pythonhosted.org/packages/9f/37/eae4830a28061ba552516d84478686b637cd9e57d6a90b45ad69e89cb0af/websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d", size = 172372 }, + { url = "https://files.pythonhosted.org/packages/46/2f/b409f8b8aa9328d5a47f7a301a43319d540d70cf036d1e6443675978a988/websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276", size = 172607 }, + { url = "https://files.pythonhosted.org/packages/d6/81/d7e2e4542d4b4df849b0110df1b1f94f2647b71ab4b65d672090931ad2bb/websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc", size = 182422 }, + { url = "https://files.pythonhosted.org/packages/b6/91/3b303160938d123eea97f58be363f7dbec76e8c59d587e07b5bc257dd584/websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72", size = 181362 }, + { url = "https://files.pythonhosted.org/packages/f2/8b/df6807f1ca339c567aba9a7ab03bfdb9a833f625e8d2b4fc7529e4c701de/websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d", size = 181787 }, + { url = "https://files.pythonhosted.org/packages/21/37/e6d3d5ebb0ebcaf98ae84904205c9dcaf3e0fe93e65000b9f08631ed7309/websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab", size = 182058 }, + { url = "https://files.pythonhosted.org/packages/c9/df/6aca296f2be4c638ad20908bb3d7c94ce7afc8d9b4b2b0780d1fc59b359c/websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99", size = 181434 }, + { url = "https://files.pythonhosted.org/packages/88/f1/75717a982bab39bbe63c83f9df0e7753e5c98bab907eb4fb5d97fe5c8c11/websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc", size = 181431 }, + { url = "https://files.pythonhosted.org/packages/e7/15/cee9e63ed9ac5bfc1a3ae8fc6c02c41745023c21eed622eef142d8fdd749/websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904", size = 175678 }, + { url = "https://files.pythonhosted.org/packages/4e/00/993974c60f40faabb725d4dbae8b072ef73b4c4454bd261d3b1d34ace41f/websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa", size = 176119 }, + { url = "https://files.pythonhosted.org/packages/12/23/be28dc1023707ac51768f848d28a946443041a348ee3a54abdf9f6283372/websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1", size = 174714 }, + { url = "https://files.pythonhosted.org/packages/8f/ff/02b5e9fbb078e7666bf3d25c18c69b499747a12f3e7f2776063ef3fb7061/websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7", size = 172374 }, + { url = "https://files.pythonhosted.org/packages/8e/61/901c8d4698e0477eff4c3c664d53f898b601fa83af4ce81946650ec2a4cb/websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081", size = 172605 }, + { url = "https://files.pythonhosted.org/packages/d2/4b/dc47601a80dff317aecf8da7b4ab278d11d3494b2c373b493e4887561f90/websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9", size = 182380 }, + { url = "https://files.pythonhosted.org/packages/83/f7/b155d2b38f05ed47a0b8de1c9ea245fcd7fc625d89f35a37eccba34b42de/websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b", size = 181325 }, + { url = "https://files.pythonhosted.org/packages/d3/ff/040a20c01c294695cac0e361caf86f33347acc38f164f6d2be1d3e007d9f/websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f", size = 181763 }, + { url = "https://files.pythonhosted.org/packages/cb/6a/af23e93678fda8341ac8775e85123425e45c608389d3514863c702896ea5/websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6", size = 182097 }, + { url = "https://files.pythonhosted.org/packages/7e/3e/1069e159c30129dc03c01513b5830237e576f47cedb888777dd885cae583/websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375", size = 181485 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c91c47103f1cd941b576bbc452601e9e01f67d5c9be3e0a9abe726491ab5/websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72", size = 181466 }, + { url = "https://files.pythonhosted.org/packages/16/32/a4ca6e3d56c24aac46b0cf5c03b841379f6409d07fc2044b244f90f54105/websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c", size = 175673 }, + { url = "https://files.pythonhosted.org/packages/c0/31/25a417a23e985b61ffa5544f9facfe4a118cb64d664c886f1244a8baeca5/websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8", size = 176115 }, + { url = "https://files.pythonhosted.org/packages/e8/b2/31eec524b53f01cd8343f10a8e429730c52c1849941d1f530f8253b6d934/websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3", size = 169023 }, ] [[package]]