diff --git a/.dockerignore b/.dockerignore index f6a9c4e..c55a48f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,5 @@ .github/ .gitignore config.example.yaml -poetry.lock -pyproject.toml readme.md renovate.json diff --git a/.github/workflows/CI-CD.yaml b/.github/workflows/CI-CD.yaml index c96b6ef..2dc759b 100644 --- a/.github/workflows/CI-CD.yaml +++ b/.github/workflows/CI-CD.yaml @@ -1,4 +1,6 @@ #file: noinspection SpellCheckingInspection +name: CI/CD + on: push: branches: [ "main", "dev" ] diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 7ddd9d5..1cbcd1e 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.9.0 - name: Log into registry ${{ env.REGISTRY }} uses: docker/login-action@v3.3.0 @@ -29,7 +29,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5.5.1 + uses: docker/metadata-action@v5.6.1 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -39,7 +39,7 @@ jobs: type=sha - name: Build and push Docker image - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.14.0 with: context: . push: true diff --git a/.github/workflows/sync_template.yaml b/.github/workflows/sync_template.yaml new file mode 100644 index 0000000..eadb1a0 --- /dev/null +++ b/.github/workflows/sync_template.yaml @@ -0,0 +1,39 @@ +name: Template Sync + +on: + schedule: + - cron: "0 0 1 * *" + workflow_dispatch: + +jobs: + repo-sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + if: github.repository != 'nicebots-xyz/botkit' + outputs: + pr_branch: ${{ steps.template-sync.outputs.pr_branch }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Template Sync + id: template-sync + uses: AndreasAugustin/actions-template-sync@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + source_repo_path: nicebots-xyz/botkit + upstream_branch: dev + pr_commit_msg: "core: :twisted_rightwards_arrows: Merge remote template" + pr_title: "core: :twisted_rightwards_arrows: Merge remote template" + + update-deps: + needs: repo-sync + if: needs.repo-sync.outputs.pr_branch + uses: ./.github/workflows/update_dependencies.yaml + with: + pr_branch: ${{ needs.repo-sync.outputs.pr_branch }} + permissions: + contents: write + pull-requests: write diff --git a/.github/workflows/update_dependencies.yaml b/.github/workflows/update_dependencies.yaml index aaffd60..8c9c96d 100644 --- a/.github/workflows/update_dependencies.yaml +++ b/.github/workflows/update_dependencies.yaml @@ -1,39 +1,126 @@ -name: Update Dependencies - +name: Update dependencies on: schedule: - cron: '0 0 * * 0' # Runs at 00:00 every Sunday - workflow_dispatch: # Allows manual triggering + workflow_dispatch: + inputs: + pr_branch: + description: 'Branch to push changes to (optional)' + required: false + type: string + workflow_call: + inputs: + pr_branch: + description: 'Branch to push changes to' + required: true + type: string jobs: - update-dependencies: + update-deps: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@v4 + name: Checkout repository + with: + fetch-depth: 0 # Fetch all history for all branches + - name: Checkout target branch + if: inputs.pr_branch + run: | + BRANCH_NAME=$(echo ${{ inputs.pr_branch }} | sed 's|refs/heads/||') + git fetch origin $BRANCH_NAME + git checkout $BRANCH_NAME + git pull origin $BRANCH_NAME + - name: Setup PDM uses: pdm-project/setup-pdm@v4 with: cache: true + - name: Lock dependencies - run: pdm lock + run: pdm lock -G :all + - name: Export requirements run: pdm run export + - name: Check for changes id: git-check run: | git diff --exit-code --quiet requirements.txt || echo "changed=true" >> $GITHUB_OUTPUT - - name: Create Pull Request + + - name: Create required label if not exists + run: | + # Check if label exists + LABEL_EXISTS=$(gh label list | grep "automated-dependencies-update" || true) + if [ -z "$LABEL_EXISTS" ]; then + echo "Creating automated-dependencies-update label..." + gh label create "automated-dependencies-update" \ + --color "2DA44E" \ + --description "Automated PR for updating project dependencies" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Handle dependency updates if: steps.git-check.outputs.changed == 'true' run: | git config user.name github-actions git config user.email github-actions@github.com - git checkout -b update-dependencies-${{ github.run_id }} - git add requirements.txt pyproject.toml pdm.lock - git commit -m "Update dependencies" - git push origin update-dependencies-${{ github.run_id }} - gh pr create --title "Update dependencies" --body "This PR updates the project dependencies. Please review the changes and merge if everything looks good." --base ${{ github.ref_name }} --head update-dependencies-${{ github.run_id }} + + # Function to commit changes + commit_changes() { + git add requirements.txt pyproject.toml pdm.lock + git commit -m "Update dependencies" + } + + # Function to create PR + create_pr() { + local BRANCH=$1 + gh pr create \ + --title "Update dependencies" \ + --body "This PR updates the project dependencies. Please review the changes and merge if everything looks good." \ + --base ${{ github.ref_name }} \ + --head $BRANCH \ + --label "automated-dependencies-update" + } + + if [ -n "${{ inputs.pr_branch }}" ]; then + # Push to existing branch + BRANCH_NAME=$(echo ${{ inputs.pr_branch }} | cut -d'/' -f 3) + git checkout $BRANCH_NAME + commit_changes + git push origin $BRANCH_NAME + else + # Check for existing PR - strict search for exact title and our specific branch pattern + EXISTING_PR=$(gh pr list --search "in:title Update dependencies is:open label:automated-dependencies-update" --json headRefName,number,author -q '.[0]') + + if [ -n "$EXISTING_PR" ]; then + # Check if PR has our automation label + BRANCH_NAME=$(echo $EXISTING_PR | jq -r .headRefName) + + if [[ "$BRANCH_NAME" == update-dependencies-* ]]; then + echo "Found valid automated PR with branch $BRANCH_NAME. Updating it." + git checkout -B $BRANCH_NAME + commit_changes + git push -f origin $BRANCH_NAME + else + echo "Found PR but wrong branch pattern. Creating new branch." + NEW_BRANCH="update-dependencies-${{ github.run_id }}" + git checkout -b $NEW_BRANCH + commit_changes + git push origin $NEW_BRANCH + create_pr $NEW_BRANCH + fi + else + echo "No existing PR found. Creating new branch and PR." + NEW_BRANCH="update-dependencies-${{ github.run_id }}" + git checkout -b $NEW_BRANCH + commit_changes + git push origin $NEW_BRANCH + create_pr $NEW_BRANCH + fi + fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..01edbcf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +ci: + autoupdate_commit_msg: "chore(pre-commit): pre-commit autoupdate" + autofix_commit_msg: "style(pre-commit): auto fixes from pre-commit.com hooks" + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude: \.(po|pot|yml|yaml)$ + - id: end-of-file-fixer + exclude: \.(po|pot|yml|yaml)$ + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + args: [--prose-wrap=always, --print-width=88] + exclude: \.(po|pot|yml|yaml)$ + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.4 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format + - repo: local + hooks: + - id: copywrite + name: copywrite + entry: copywrite headers + language: system + pass_filenames: false + files: . diff --git a/.templatesyncignore b/.templatesyncignore new file mode 100644 index 0000000..2a0b916 --- /dev/null +++ b/.templatesyncignore @@ -0,0 +1,3 @@ +.copywrite.hcl +requirements.txt +pdm.lock diff --git a/Dockerfile b/Dockerfile index 875398b..78d6950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,35 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -# For more information, please refer to https://aka.ms/vscode-docker-python -FROM python:3.12-slim-bookworm +ARG PYTHON_VERSION=3.12 +ARG NODE_VERSION=20 +FROM python:${PYTHON_VERSION}-slim-bookworm AS python-base -# Keeps Python from generating .pyc files in the container ENV PYTHONDONTWRITEBYTECODE=1 - -# Turns off buffering for easier container logging ENV PYTHONUNBUFFERED=1 -# we move to the app folder and run the pip install command +RUN pip install -U pdm +ENV PDM_CHECK_UPDATE=false + WORKDIR /app +COPY src pyproject.toml pdm.lock ./ -ENV PYTHONUNBUFFERED 1 -ENV PYTHONDONTWRITEBYTECODE 1 +RUN pdm export --prod -o requirements.txt -# we copy just the requirements.txt first to leverage Docker cache -COPY requirements.txt . +FROM python:${PYTHON_VERSION}-slim-bookworm AS app -# Install pip requirements -RUN pip install -r requirements.txt +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app -# Creates a non-root user with an explicit UID and adds permission to access the /app folder RUN adduser -u 6392 --disabled-password --gecos "" appuser && chown -R appuser /app -USER appuser -# We copy the rest of the codebase into the image -COPY ./ /app/ +COPY --from=python-base --chown=appuser /app/requirements.txt ./ +COPY src/ ./src +COPY LICENSE ./ + +RUN pip install -r requirements.txt --require-hashes +USER appuser -# We run the application -CMD ["python", "src"] \ No newline at end of file +CMD ["python", "src"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..0977c21 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,9 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +services: + redis: + image: redis:alpine + ports: + - "6379:6379" + command: --loglevel debug \ No newline at end of file diff --git a/config.example.yaml b/config.example.yaml index 115e952..97db981 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -36,10 +36,16 @@ extensions: every: 300 # 300 seconds = 5 minutes bot: token: "bot token here" # Your bot token + cache: + type: "memory" # Cache type. Possible values: "memory" or "redis" + redis: # Redis configuration (only used if type is "redis") + host: "localhost" # Redis server host + port: 6379 # Redis server port + db: 0 # Redis database number + password: null # Redis password (optional) + ssl: false # Whether to use SSL for Redis connection logging: level: "INFO" # The logging level. Possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL use: bot: true # Whether to run the bot backend: false # Whether to run the backend - - diff --git a/guides/getting-started.md b/guides/getting-started.md index 58827e2..45d108f 100644 --- a/guides/getting-started.md +++ b/guides/getting-started.md @@ -1,6 +1,8 @@ # Getting Started with Botkit and Pycord: Creating Your First Bot Extension -This comprehensive tutorial will guide you through the process of setting up Botkit, creating your first bot extension using Pycord, and understanding the core concepts of Discord bot development. +This comprehensive tutorial will guide you through the process of setting up Botkit, +creating your first bot extension using Pycord, and understanding the core concepts of +Discord bot development. ## Prerequisites @@ -10,18 +12,21 @@ Before we begin, ensure you have the following: 2. Basic understanding of Python and Discord concepts 3. A Discord account and access to the Discord Developer Portal -> [!IMPORTANT] -> If you haven't already, create a Discord application and bot user in the [Discord Developer Portal](https://discord.com/developers/applications). You'll need the bot token for later steps. +> [!IMPORTANT] If you haven't already, create a Discord application and bot user in the +> [Discord Developer Portal](https://discord.com/developers/applications). You'll need +> the bot token for later steps. ## Step 1: Install Git -If you don't have Git installed, you'll need to install it to clone the Botkit repository. +If you don't have Git installed, you'll need to install it to clone the Botkit +repository. -1. Visit the [Git website](https://git-scm.com/downloads) and download the appropriate version for your operating system. +1. Visit the [Git website](https://git-scm.com/downloads) and download the appropriate + version for your operating system. 2. Follow the installation instructions for your OS. -> [!TIP] -> On Windows, you can use the Git Bash terminal that comes with Git for a Unix-like command-line experience. +> [!TIP] On Windows, you can use the Git Bash terminal that comes with Git for a +> Unix-like command-line experience. To verify Git is installed correctly, open a terminal or command prompt and run: @@ -49,12 +54,13 @@ git clone https://github.com/nicebots-xyz/botkit cd botkit ``` -> [!NOTE] -> Cloning the repository creates a local copy of Botkit on your machine, allowing you to build your bot using the Botkit framework. +> [!NOTE] Cloning the repository creates a local copy of Botkit on your machine, +> allowing you to build your bot using the Botkit framework. ## Step 3: Set Up a Virtual Environment (Optional but Recommended) -It's a good practice to use a virtual environment for your Python projects. This keeps your project dependencies isolated from your system-wide Python installation. +It's a good practice to use a virtual environment for your Python projects. This keeps +your project dependencies isolated from your system-wide Python installation. 1. Create a virtual environment: @@ -63,17 +69,17 @@ python -m venv venv ``` 2. Activate the virtual environment: - - On Windows: - ``` - venv\Scripts\activate - ``` - - On macOS and Linux: - ``` - source venv/bin/activate - ``` - -> [!TIP] -> You'll know the virtual environment is active when you see `(venv)` at the beginning of your terminal prompt. + - On Windows: + ``` + venv\Scripts\activate + ``` + - On macOS and Linux: + ``` + source venv/bin/activate + ``` + +> [!TIP] You'll know the virtual environment is active when you see `(venv)` at the +> beginning of your terminal prompt. ## Step 4: Install Dependencies @@ -91,8 +97,8 @@ pip install pdm pdm install ``` -> [!NOTE] -> PDM will read the `pyproject.toml` file and install all necessary dependencies for Botkit. +> [!NOTE] PDM will read the `pyproject.toml` file and install all necessary dependencies +> for Botkit. ## Step 5: Configure Your Bot @@ -106,8 +112,8 @@ bot: Replace `YOUR_BOT_TOKEN_HERE` with the actual token of your Discord bot. -> [!CAUTION] -> Never share your bot token publicly or commit it to version control. Treat it like a password. +> [!CAUTION] Never share your bot token publicly or commit it to version control. Treat +> it like a password. ## Step 6: Create a New Extension Folder @@ -138,8 +144,9 @@ from .main import setup, default, schema __all__ = ["setup", "default", "schema"] ``` -> [!NOTE] -> This file imports and exposes the necessary components from our `main.py` file (which we'll create next). It allows Botkit to access these components when loading the extension. +> [!NOTE] This file imports and exposes the necessary components from our `main.py` file +> (which we'll create next). It allows Botkit to access these components when loading +> the extension. ## Step 8: Create the `main.py` File @@ -177,17 +184,20 @@ schema = { Let's break down what we've done here: - We import the necessary modules from discord and discord.ext. -- We use `typing` to add type hints, which improves code readability and helps catch errors early. -- We define a `MyFirstExtension` class that inherits from `commands.Cog`. This class will contain our commands and listeners. +- We use `typing` to add type hints, which improves code readability and helps catch + errors early. +- We define a `MyFirstExtension` class that inherits from `commands.Cog`. This class + will contain our commands and listeners. - The `setup` function is required by Botkit to add our cog to the bot. - We define `default` and `schema` dictionaries for the extension's configuration. -> [!TIP] -> Using type hints (like `bot: discord.Bot`) helps catch errors early and improves code readability. It's a good practice to use them consistently in your code. +> [!TIP] Using type hints (like `bot: discord.Bot`) helps catch errors early and +> improves code readability. It's a good practice to use them consistently in your code. ## Step 9: Adding Commands -Now, let's add some commands to our extension. We'll create a simple "hello" command and a more complex "userinfo" command. +Now, let's add some commands to our extension. We'll create a simple "hello" command and +a more complex "userinfo" command. Add the following methods to your `MyFirstExtension` class in `main.py`: @@ -215,22 +225,27 @@ async def userinfo( Let's explain these commands: 1. The `hello` command: - - Uses the `@discord.slash_command` decorator to create a slash command. - - Takes only the `ctx` (context) parameter, which is automatically provided by Discord. - - Responds with a greeting using the author's name. + + - Uses the `@discord.slash_command` decorator to create a slash command. + - Takes only the `ctx` (context) parameter, which is automatically provided by + Discord. + - Responds with a greeting using the author's name. 2. The `userinfo` command: - - Also uses `@discord.slash_command` to create a slash command. - - Takes an optional `user` parameter, which defaults to the command author if not provided. - - Creates an embed with various pieces of information about the user. - - Responds with the created embed. + - Also uses `@discord.slash_command` to create a slash command. + - Takes an optional `user` parameter, which defaults to the command author if not + provided. + - Creates an embed with various pieces of information about the user. + - Responds with the created embed. -> [!NOTE] -> Slash commands are the modern way to create Discord bot commands. They provide better user experience and are easier to discover than traditional prefix-based commands. +> [!NOTE] Slash commands are the modern way to create Discord bot commands. They provide +> better user experience and are easier to discover than traditional prefix-based +> commands. ## Step 10: Adding an Event Listener -Let's add an event listener to our extension to demonstrate how to respond to Discord events. We'll add a simple listener that logs when the bot is ready. +Let's add an event listener to our extension to demonstrate how to respond to Discord +events. We'll add a simple listener that logs when the bot is ready. Add the following method to your `MyFirstExtension` class in `main.py`: @@ -240,10 +255,11 @@ async def on_ready(self): print(f"Bot is ready! Logged in as {self.bot.user}") ``` -This listener will print a message to the console when the bot has successfully connected to Discord. +This listener will print a message to the console when the bot has successfully +connected to Discord. -> [!TIP] -> Event listeners are great for performing actions based on Discord events, such as when a member joins a server or when a message is deleted. +> [!TIP] Event listeners are great for performing actions based on Discord events, such +> as when a member joins a server or when a message is deleted. ## Step 11: Final `main.py` File @@ -304,25 +320,29 @@ Now that we've created our extension, let's run the bot: pdm run start ``` -> [!IMPORTANT] -> Ensure your bot token is correctly set in the `config.yml` file before running the bot. +> [!IMPORTANT] Ensure your bot token is correctly set in the `config.yml` file before +> running the bot. -If everything is set up correctly, you should see the "Bot is ready!" message in your console, indicating that your bot is now online and ready to respond to commands. +If everything is set up correctly, you should see the "Bot is ready!" message in your +console, indicating that your bot is now online and ready to respond to commands. ## Conclusion -Congratulations! You've now created your first bot extension using Botkit and Pycord. This extension includes: +Congratulations! You've now created your first bot extension using Botkit and Pycord. +This extension includes: 1. A simple "hello" slash command 2. A more complex "userinfo" slash command that creates an embed 3. An event listener for the "on_ready" event -> [!TIP] -> To continue improving your bot, consider adding more commands, implementing additional event listeners, or integrating with external APIs or databases. +> [!TIP] To continue improving your bot, consider adding more commands, implementing +> additional event listeners, or integrating with external APIs or databases. -> [!WARNING] -> Always be cautious when handling user data and permissions in your bot. Ensure you're following Discord's Terms of Service and Developer Policy. +> [!WARNING] Always be cautious when handling user data and permissions in your bot. +> Ensure you're following Discord's Terms of Service and Developer Policy. -Remember to always use type hinting in your code. It helps with code readability, catches potential errors early, and provides better autocomplete suggestions in many IDEs. +Remember to always use type hinting in your code. It helps with code readability, +catches potential errors early, and provides better autocomplete suggestions in many +IDEs. -Happy coding, and enjoy building your Discord bot! \ No newline at end of file +Happy coding, and enjoy building your Discord bot! diff --git a/pdm.lock b/pdm.lock index 699ae27..d8c0def 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,24 +2,87 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "dev", "sentry"] -strategy = ["cross_platform", "inherit_metadata"] +groups = ["default", "dev"] +strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:56ec6f407361c56965236da39d47a2d56bc7c96d00e66e2c961a1f6e6066f5fe" +content_hash = "sha256:c1e04410645b8e9502370dd365744a48b082c15e9dc58ce35797246cc3d2b34f" [[metadata.targets]] -requires_python = "==3.11.*" +requires_python = "==3.12.*" + +[[package]] +name = "aerich" +version = "0.8.1" +requires_python = "<4.0,>=3.8" +summary = "A database migrations tool for Tortoise ORM." +groups = ["default"] +dependencies = [ + "asyncclick<9.0.0.0,>=8.1.7.2", + "dictdiffer", + "pydantic!=2.7.0,<3.0,>=2.0", + "tortoise-orm>=0.21", +] +files = [ + {file = "aerich-0.8.1-py3-none-any.whl", hash = "sha256:2743cf85bd9957ea173055dad07ee5a3219067e4f117d5402a44204c27e83c9f"}, + {file = "aerich-0.8.1.tar.gz", hash = "sha256:1e95b1c04dfc0c634dd43b0123933038c820140e17a4b27885a63b7461eb0632"}, +] + +[[package]] +name = "aerich" +version = "0.8.1" +extras = ["toml"] +requires_python = "<4.0,>=3.8" +summary = "A database migrations tool for Tortoise ORM." +groups = ["default"] +dependencies = [ + "aerich==0.8.1", + "tomli-w<2.0.0,>=1.1.0; python_version >= \"3.11\"", + "tomlkit; python_version < \"3.11\"", +] +files = [ + {file = "aerich-0.8.1-py3-none-any.whl", hash = "sha256:2743cf85bd9957ea173055dad07ee5a3219067e4f117d5402a44204c27e83c9f"}, + {file = "aerich-0.8.1.tar.gz", hash = "sha256:1e95b1c04dfc0c634dd43b0123933038c820140e17a4b27885a63b7461eb0632"}, +] + +[[package]] +name = "aiocache" +version = "0.12.3" +summary = "multi backend asyncio cache" +groups = ["default"] +files = [ + {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"}, + {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"}, +] [[package]] name = "aiocache" version = "0.12.3" +extras = ["redis"] summary = "multi backend asyncio cache" groups = ["default"] +dependencies = [ + "aiocache==0.12.3", + "redis>=4.2.0", +] files = [ {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"}, {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"}, ] +[[package]] +name = "aiofile" +version = "3.9.0" +requires_python = "<4,>=3.8" +summary = "Asynchronous file operations." +groups = ["default"] +dependencies = [ + "caio<0.10.0,>=0.9.0", +] +files = [ + {file = "aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa"}, + {file = "aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b"}, +] + [[package]] name = "aiofiles" version = "24.1.0" @@ -33,18 +96,18 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.4.4" -requires_python = ">=3.8" +version = "2.4.6" +requires_python = ">=3.9" summary = "Happy Eyeballs for asyncio" groups = ["default"] files = [ - {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, - {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, + {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"}, + {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"}, ] [[package]] name = "aiohttp" -version = "3.11.8" +version = "3.11.12" requires_python = ">=3.9" summary = "Async http client/server framework (asyncio)" groups = ["default"] @@ -59,47 +122,62 @@ dependencies = [ "yarl<2.0,>=1.17.0", ] files = [ - {file = "aiohttp-3.11.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8dd02b44555893adfe7cc4b3b454fee04f9dcec45cf66ef5bb53ebf393f0505"}, - {file = "aiohttp-3.11.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658052941324edea3dee1f681375e70779f55e437e07bdfc4b5bbe65ad53cefb"}, - {file = "aiohttp-3.11.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c829471a9e2266da4a0666f8a9e215f19320f79778af379c1c7db324ac24ed2"}, - {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d21951756690f5d86d0215da38eb0fd65def03b5e2a1c08a4a39718a6d0d48f2"}, - {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fa50ddc6b21cc1ae23e13524d6f75b27e279fdf5cf905b2df6fd171891ac4e2"}, - {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a5afbd805e449048ecebb1a256176e953d4ca9e48bab387d4d1c8524f1c7a95"}, - {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea68db69f2a4ddc24b28b8e754fc0b963ed7f9b9a76137f06fe44643d6821fbd"}, - {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b3ac163145660ce660aed2f1005e6d4de840d39728990b7250525eeec4e4a8"}, - {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9ac0cce897904b77e109e5403ed713187dbdf96832bfd061ac07164264be16c"}, - {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3260c77cff4e35245bc517658bd54d7a64787f71f3c4f723877c82f22835b032"}, - {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7fd9c11ffad6b022bf02a41a70418cb2ab3b33f2c27842a5999e3ab78daf280"}, - {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16bda233a7b159ab08107e8858fedca90a9de287057fab54cafde51bd83f9819"}, - {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4867008617bbf86e9fb5b00f72dd0e3a00a579b32233caff834320867f9b7cac"}, - {file = "aiohttp-3.11.8-cp311-cp311-win32.whl", hash = "sha256:17e6b9d8e29e3bfc7f893f327e92c9769d3582cee2fb1652c1431ac3f60115a0"}, - {file = "aiohttp-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:7f3be4961a5c2c670f31caab7641a37ea2a97031f0d8ae15bcfd36b6bf273200"}, - {file = "aiohttp-3.11.8.tar.gz", hash = "sha256:7bc9d64a2350cbb29a9732334e1a0743cbb6844de1731cbdf5949b235653f3fd"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"}, + {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"}, + {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"}, + {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"}, ] [[package]] name = "aiolimiter" -version = "1.1.1" +version = "1.2.1" requires_python = "<4.0,>=3.8" summary = "asyncio rate limiter, a leaky bucket implementation" groups = ["default"] files = [ - {file = "aiolimiter-1.1.1-py3-none-any.whl", hash = "sha256:bf23dafbd1370e0816792fbcfb8fb95d5138c26e05f839fe058f5440bea006f5"}, - {file = "aiolimiter-1.1.1.tar.gz", hash = "sha256:4b5740c96ecf022d978379130514a26c18001e7450ba38adf19515cd0970f68f"}, + {file = "aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7"}, + {file = "aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9"}, ] [[package]] name = "aiosignal" -version = "1.3.1" -requires_python = ">=3.7" +version = "1.3.2" +requires_python = ">=3.9" summary = "aiosignal: a list of registered asynchronous callbacks" groups = ["default"] dependencies = [ "frozenlist>=1.1.0", ] files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[[package]] +name = "aiosqlite" +version = "0.20.0" +requires_python = ">=3.8" +summary = "asyncio bridge to the standard sqlite3 module" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0", +] +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, ] [[package]] @@ -117,22 +195,83 @@ files = [ ] [[package]] -name = "attrs" -version = "24.2.0" -requires_python = ">=3.7" -summary = "Classes Without Boilerplate" +name = "anyio" +version = "4.8.0" +requires_python = ">=3.9" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" groups = ["default"] dependencies = [ - "importlib-metadata; python_version < \"3.8\"", + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +groups = ["default"] +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "asyncclick" +version = "8.1.8" +requires_python = ">=3.9" +summary = "Composable command line interface toolkit, " +groups = ["default"] +dependencies = [ + "anyio~=4.0", + "colorama; platform_system == \"Windows\"", ] files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "asyncclick-8.1.8-py3-none-any.whl", hash = "sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6"}, + {file = "asyncclick-8.1.8.0-py3-none-any.whl", hash = "sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678"}, + {file = "asyncclick-8.1.8.tar.gz", hash = "sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c"}, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +requires_python = ">=3.8.0" +summary = "An asyncio PostgreSQL driver" +groups = ["default"] +dependencies = [ + "async-timeout>=4.0.3; python_version < \"3.11.0\"", +] +files = [ + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[[package]] +name = "attrs" +version = "25.1.0" +requires_python = ">=3.8" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [[package]] name = "basedpyright" -version = "1.22.0" +version = "1.27.1" requires_python = ">=3.8" summary = "static type checking for Python (but based)" groups = ["dev"] @@ -140,22 +279,23 @@ dependencies = [ "nodejs-wheel-binaries>=20.13.1", ] files = [ - {file = "basedpyright-1.22.0-py3-none-any.whl", hash = "sha256:6376107086ad25525429b8a94a2ffdb67c6dd2b1a6be38bf3c6ea9b5c3d1f688"}, - {file = "basedpyright-1.22.0.tar.gz", hash = "sha256:457e97ac4c3f694b900453d3f8824af36d17b9cef3d76c623e665dd4c7872d9c"}, + {file = "basedpyright-1.27.1-py3-none-any.whl", hash = "sha256:9f3647c1c8a0fc3d0a08b2156768ff717322f1e78d1b45686597093824a8f0d3"}, + {file = "basedpyright-1.27.1.tar.gz", hash = "sha256:ddbe226c154c973a954626ce9ecb3b3a59df9d68bbab0e3f82a6c2c7b29616df"}, ] [[package]] name = "beautifulsoup4" -version = "4.12.3" -requires_python = ">=3.6.0" +version = "4.13.3" +requires_python = ">=3.7.0" summary = "Screen-scraping library" groups = ["default", "dev"] dependencies = [ "soupsieve>1.2", + "typing-extensions>=4.0.0", ] files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, + {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, + {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, ] [[package]] @@ -182,46 +322,57 @@ files = [ {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, ] +[[package]] +name = "caio" +version = "0.9.21" +requires_python = "<4,>=3.7" +summary = "Asynchronous file IO for Linux MacOS or Windows." +groups = ["default"] +files = [ + {file = "caio-0.9.21-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e7314a28d69a0397d1d0ac6c7bfe7973f27f4f7216cf42b0358d7d9ba27bc4cd"}, + {file = "caio-0.9.21-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:1f4241e2b89f31e1fea342c8da9a987fef71d093df7ba1169f469d0be7592608"}, + {file = "caio-0.9.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:fdab8c817b6835d997db1532ce7d9f5cbe186265bf0ee9a9840b378aa4a1cba7"}, + {file = "caio-0.9.21.tar.gz", hash = "sha256:4f1d30ad0f975de07b4a3ae1cd2e9275fa574f2ca0b49ba5ab16208575650a92"}, +] + [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.1.31" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default", "sentry"] +groups = ["default"] files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] name = "charset-normalizer" -version = "3.4.0" -requires_python = ">=3.7.0" +version = "3.4.1" +requires_python = ">=3.7" summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." groups = ["default"] files = [ - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" requires_python = ">=3.7" summary = "Composable command line interface toolkit" groups = ["default"] @@ -230,8 +381,8 @@ dependencies = [ "importlib-metadata; python_version < \"3.8\"", ] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [[package]] @@ -260,9 +411,20 @@ files = [ {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, ] +[[package]] +name = "cssselect" +version = "1.2.0" +requires_python = ">=3.7" +summary = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +groups = ["default"] +files = [ + {file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"}, + {file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"}, +] + [[package]] name = "deprecated" -version = "1.2.15" +version = "1.2.18" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" summary = "Python @deprecated decorator to deprecate old python classes, functions or methods." groups = ["dev"] @@ -270,8 +432,32 @@ dependencies = [ "wrapt<2,>=1.10", ] files = [ - {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, - {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[[package]] +name = "dictdiffer" +version = "0.9.0" +summary = "Dictdiffer is a library that helps you to diff and patch dictionaries." +groups = ["default"] +files = [ + {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, + {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, +] + +[[package]] +name = "fake-useragent" +version = "2.0.3" +requires_python = ">=3.9" +summary = "Up-to-date simple useragent faker with real world database" +groups = ["default"] +dependencies = [ + "importlib-resources>=6.0; python_version < \"3.10\"", +] +files = [ + {file = "fake_useragent-2.0.3-py3-none-any.whl", hash = "sha256:8bae50abb72c309a5b3ae2f01a0b82426613fd5c4e2a04dca9332399ec44daa1"}, + {file = "fake_useragent-2.0.3.tar.gz", hash = "sha256:af86a26ef8229efece8fed529b4aeb5b73747d889b60f01cd477b6f301df46e6"}, ] [[package]] @@ -300,21 +486,21 @@ requires_python = ">=3.8" summary = "A list-like structure which implements collections.abc.MutableSequence" groups = ["default"] files = [ - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, - {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, - {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] @@ -335,28 +521,28 @@ files = [ [[package]] name = "h2" -version = "4.1.0" -requires_python = ">=3.6.1" -summary = "HTTP/2 State-Machine based protocol implementation" +version = "4.2.0" +requires_python = ">=3.9" +summary = "Pure-Python HTTP/2 protocol implementation" groups = ["default"] dependencies = [ - "hpack<5,>=4.0", - "hyperframe<7,>=6.0", + "hpack<5,>=4.1", + "hyperframe<7,>=6.1", ] files = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, ] [[package]] name = "hpack" -version = "4.0.0" -requires_python = ">=3.6.1" -summary = "Pure-Python HPACK header compression" +version = "4.1.0" +requires_python = ">=3.9" +summary = "Pure-Python HPACK header encoding" groups = ["default"] files = [ - {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, - {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, ] [[package]] @@ -398,13 +584,13 @@ files = [ [[package]] name = "hyperframe" -version = "6.0.1" -requires_python = ">=3.6.1" -summary = "HTTP/2 framing layer for Python" +version = "6.1.0" +requires_python = ">=3.9" +summary = "Pure-Python HTTP/2 framing" groups = ["default"] files = [ - {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, - {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, ] [[package]] @@ -429,6 +615,17 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "iso8601" +version = "2.1.0" +requires_python = ">=3.7,<4.0" +summary = "Simple module to parse ISO 8601 dates" +groups = ["default"] +files = [ + {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, + {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -442,7 +639,7 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" requires_python = ">=3.7" summary = "A very fast and expressive template engine." groups = ["default"] @@ -450,8 +647,80 @@ dependencies = [ "MarkupSafe>=2.0", ] files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[[package]] +name = "lxml" +version = "5.3.1" +requires_python = ">=3.6" +summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +groups = ["default"] +files = [ + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, + {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, + {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, + {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, +] + +[[package]] +name = "lxml-html-clean" +version = "0.4.1" +summary = "HTML cleaner from lxml project" +groups = ["default"] +dependencies = [ + "lxml", +] +files = [ + {file = "lxml_html_clean-0.4.1-py3-none-any.whl", hash = "sha256:b704f2757e61d793b1c08bf5ad69e4c0b68d6696f4c3c1429982caf90050bcaf"}, + {file = "lxml_html_clean-0.4.1.tar.gz", hash = "sha256:40c838bbcf1fc72ba4ce811fbb3135913017b27820d7c16e8bc412ae1d8bc00b"}, +] + +[[package]] +name = "lxml" +version = "5.3.1" +extras = ["html-clean"] +requires_python = ">=3.6" +summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +groups = ["default"] +dependencies = [ + "lxml-html-clean", + "lxml==5.3.1", +] +files = [ + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, + {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, + {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, + {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, ] [[package]] @@ -475,16 +744,16 @@ requires_python = ">=3.9" summary = "Safely add untrusted strings to HTML/XML markup." groups = ["default"] files = [ - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] @@ -509,78 +778,83 @@ dependencies = [ "typing-extensions>=4.1.0; python_version < \"3.11\"", ] files = [ - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, - {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, - {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] [[package]] name = "nodejs-wheel-binaries" -version = "22.11.0" +version = "22.14.0" requires_python = ">=3.7" summary = "unoffical Node.js package" groups = ["dev"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] files = [ - {file = "nodejs_wheel_binaries-22.11.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:00afada277fd6e945a74f881831aaf1bb7f853a15e15e8c998238ab88d327f6a"}, - {file = "nodejs_wheel_binaries-22.11.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:f29471263d65a66520a04a0e74ff641a775df1135283f0b4d1826048932b289d"}, - {file = "nodejs_wheel_binaries-22.11.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2ff0e20389f22927e311ccab69c1ecb34c3431fa809d1548e7000dc8248680"}, - {file = "nodejs_wheel_binaries-22.11.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9545cc43f1ba2c9f467f3444e9cd7f8db059933be1a5215135610dee5b38bf3"}, - {file = "nodejs_wheel_binaries-22.11.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:43a277cbabf4b68e0a4798578a4f17e5f518ada1f79a174bfde06eb2bb47e730"}, - {file = "nodejs_wheel_binaries-22.11.0-py2.py3-none-win_amd64.whl", hash = "sha256:8310ab182ee159141e08c85bc07f11e67ac3044922e6e4958f4a8f3ba6860185"}, - {file = "nodejs_wheel_binaries-22.11.0.tar.gz", hash = "sha256:e67f4e4a646bba24baa2150460c9cfbde0f75169ba37e58a2341930a5c1456ee"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d8ab8690516a3e98458041286e3f0d6458de176d15c14f205c3ea2972131420d"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:b2f200f23b3610bdbee01cf136279e005ffdf8ee74557aa46c0940a7867956f6"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0877832abd7a9c75c8c5caafa37f986c9341ee025043c2771213d70c4c1defa"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fded5a70a8a55c2135e67bd580d8b7f2e94fcbafcc679b6a2d5b92f88373d69"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c1ade6f3ece458b40c02e89c91d5103792a9f18aaad5026da533eb0dcb87090e"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34fa5ed4cf3f65cbfbe9b45c407ffc2fc7d97a06cd8993e6162191ff81f29f48"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:ca7023276327455988b81390fa6bbfa5191c1da7fc45bc57c7abc281ba9967e9"}, + {file = "nodejs_wheel_binaries-22.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:fd59c8e9a202221e316febe1624a1ae3b42775b7fb27737bf12ec79565983eaf"}, + {file = "nodejs_wheel_binaries-22.14.0.tar.gz", hash = "sha256:c1dc43713598c7310d53795c764beead861b8c5021fe4b1366cb912ce1a4c8bf"}, ] [[package]] name = "nodriver" -version = "0.38.post1" +version = "0.39" requires_python = ">=3.9" summary = "[Docs here](https://ultrafunkamsterdam.github.io/nodriver)" groups = ["dev"] dependencies = [ "deprecated", "mss", - "websockets<=13.1", + "websockets>=14", ] files = [ - {file = "nodriver-0.38.post1-py3-none-any.whl", hash = "sha256:ce82832fb2b2c8610f05c6d8f264a6c40664c31d9ca9d1595eed11da7208151b"}, - {file = "nodriver-0.38.post1.tar.gz", hash = "sha256:7df680e309b0c0f087d4ac0103556794b7fadc081b63182e44e0fa274f28ff8a"}, + {file = "nodriver-0.39-py3-none-any.whl", hash = "sha256:f245be52e6328393ece340a6dcbc8d5754fd7cf0838f0e1e40076944617178fc"}, + {file = "nodriver-0.39.tar.gz", hash = "sha256:af84f76215877c74166f95c8e7615268e31f6118f4c7291d201f29003f2248ef"}, ] [[package]] name = "orjson" -version = "3.10.12" +version = "3.10.15" requires_python = ">=3.8" summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" groups = ["default"] files = [ - {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"}, - {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"}, - {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"}, - {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"}, - {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"}, - {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"}, + {file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"}, + {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"}, + {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"}, + {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"}, + {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"}, + {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"}, + {file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"}, + {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"}, + {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"}, + {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"}, + {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"}, + {file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"}, + {file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"}, + {file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"}, ] [[package]] @@ -594,6 +868,16 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "parse" +version = "1.20.2" +summary = "parse() is the opposite of format()" +groups = ["default"] +files = [ + {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, + {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -618,29 +902,29 @@ files = [ [[package]] name = "propcache" -version = "0.2.0" -requires_python = ">=3.8" +version = "0.3.0" +requires_python = ">=3.9" summary = "Accelerated property cache" groups = ["default"] files = [ - {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, - {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, - {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, - {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, - {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, + {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e"}, + {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af"}, + {file = "propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5"}, + {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b"}, + {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667"}, + {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7"}, + {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7"}, + {file = "propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf"}, + {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138"}, + {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86"}, + {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d"}, + {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e"}, + {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64"}, + {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c"}, + {file = "propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d"}, + {file = "propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57"}, + {file = "propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043"}, + {file = "propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5"}, ] [[package]] @@ -660,23 +944,23 @@ files = [ [[package]] name = "pydantic" -version = "2.10.2" +version = "2.10.6" requires_python = ">=3.8" summary = "Data validation using Python type hints" groups = ["default"] dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.27.1", + "pydantic-core==2.27.2", "typing-extensions>=4.12.2", ] files = [ - {file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"}, - {file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"}, + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, ] [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" requires_python = ">=3.8" summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] @@ -684,35 +968,92 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, - {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, - {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, - {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, - {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[[package]] +name = "pyee" +version = "12.1.1" +requires_python = ">=3.8" +summary = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +groups = ["default"] +dependencies = [ + "typing-extensions", +] +files = [ + {file = "pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef"}, + {file = "pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3"}, ] [[package]] name = "pypartpicker" -version = "1.9.5" -summary = "A package that scrapes pcpartpicker.com and returns the results as objects." +version = "2.0.5" +requires_python = "<4.0,>=3.10" +summary = "A PCPartPicker data extractor for Python." groups = ["default"] dependencies = [ - "bs4", - "requests", + "lxml[html-clean]<6.0.0,>=5.3.0", + "requests-html<0.11.0,>=0.10.0", +] +files = [ + {file = "pypartpicker-2.0.5-py3-none-any.whl", hash = "sha256:738fbec9f0dc1226fd6926694b8f189f20bd794d2473765aa4205e7f6015b2c3"}, + {file = "pypartpicker-2.0.5.tar.gz", hash = "sha256:27c30e50d0f581bdef82c3966901ae9cacc15c9f0748a480dc70a56feb936bde"}, +] + +[[package]] +name = "pypika-tortoise" +version = "0.5.0" +requires_python = "<4.0,>=3.8" +summary = "Forked from pypika and streamline just for tortoise-orm" +groups = ["default"] +files = [ + {file = "pypika_tortoise-0.5.0-py3-none-any.whl", hash = "sha256:dbdc47eb52ce17407b05ce9f8560ce93b856d7b28beb01971d956b017846691f"}, + {file = "pypika_tortoise-0.5.0.tar.gz", hash = "sha256:ed0f56761868dc222c03e477578638590b972280b03c7c45cd93345b18b61f58"}, +] + +[[package]] +name = "pyppeteer" +version = "0.0.25" +requires_python = ">=3.5" +summary = "Headless chrome/chromium automation library (unofficial port of puppeteer)" +groups = ["default"] +dependencies = [ + "appdirs", + "pyee", + "tqdm", + "urllib3", + "websockets", +] +files = [ + {file = "pyppeteer-0.0.25.tar.gz", hash = "sha256:51fe769b722a1718043b74d12c20420f29e0dd9eeea2b66652b7f93a9ad465dd"}, +] + +[[package]] +name = "pyquery" +version = "2.0.1" +summary = "A jquery-like library for python" +groups = ["default"] +dependencies = [ + "cssselect>=1.2.0", + "lxml>=2.1", ] files = [ - {file = "pypartpicker-1.9.5-py3-none-any.whl", hash = "sha256:28cdb332c3f9b07b59caa52e39387d5dc6e5fee2f406eb6e74784cc8a635db1d"}, - {file = "pypartpicker-1.9.5.tar.gz", hash = "sha256:275a93d47253e9cd5fc9e2588de3d7a1190618b58f48b85bdcb0384c1ef534f7"}, + {file = "pyquery-2.0.1-py3-none-any.whl", hash = "sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0"}, + {file = "pyquery-2.0.1.tar.gz", hash = "sha256:0194bb2706b12d037db12c51928fe9ebb36b72d9e719565daba5a6c595322faf"}, ] [[package]] @@ -729,7 +1070,7 @@ files = [ [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" groups = ["dev"] @@ -742,8 +1083,8 @@ dependencies = [ "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [[package]] @@ -759,12 +1100,12 @@ files = [ [[package]] name = "pytz" -version = "2024.2" +version = "2025.1" summary = "World timezone definitions, modern and historical" groups = ["default"] files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, ] [[package]] @@ -774,40 +1115,54 @@ requires_python = ">=3.8" summary = "YAML parser and emitter for Python" groups = ["default"] files = [ - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "quart" -version = "0.19.9" -requires_python = ">=3.8" -summary = "A Python ASGI web microframework with the same API as Flask" +version = "0.20.0" +requires_python = ">=3.9" +summary = "A Python ASGI web framework with the same API as Flask" groups = ["default"] dependencies = [ "aiofiles", "blinker>=1.6", - "click>=8.0.0", - "flask>=3.0.0", + "click>=8.0", + "flask>=3.0", "hypercorn>=0.11.2", "importlib-metadata; python_version < \"3.10\"", "itsdangerous", "jinja2", "markupsafe", "typing-extensions; python_version < \"3.10\"", - "werkzeug>=3.0.0", + "werkzeug>=3.0", +] +files = [ + {file = "quart-0.20.0-py3-none-any.whl", hash = "sha256:003c08f551746710acb757de49d9b768986fd431517d0eb127380b656b98b8f1"}, + {file = "quart-0.20.0.tar.gz", hash = "sha256:08793c206ff832483586f5ae47018c7e40bdd75d886fee3fabbdaa70c2cf505d"}, +] + +[[package]] +name = "redis" +version = "5.2.1" +requires_python = ">=3.8" +summary = "Python client for Redis database and key-value store" +groups = ["default"] +dependencies = [ + "async-timeout>=4.0.3; python_full_version < \"3.11.3\"", ] files = [ - {file = "quart-0.19.9-py3-none-any.whl", hash = "sha256:8acb8b299c72b66ee9e506ae141498bbbfcc250b5298fbdb712e97f3d7e4082f"}, - {file = "quart-0.19.9.tar.gz", hash = "sha256:30a61a0d7bae1ee13e6e99dc14c929b3c945e372b9445d92d21db053e91e95a5"}, + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, ] [[package]] @@ -827,31 +1182,51 @@ files = [ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] +[[package]] +name = "requests-html" +version = "0.10.0" +requires_python = ">=3.6.0" +summary = "HTML Parsing for Humans." +groups = ["default"] +dependencies = [ + "bs4", + "fake-useragent", + "parse", + "pyppeteer>=0.0.14", + "pyquery", + "requests", + "w3lib", +] +files = [ + {file = "requests-html-0.10.0.tar.gz", hash = "sha256:7e929ecfed95fb1d0994bb368295d6d7c4d06b03fcb900c33d7d0b17e6003947"}, + {file = "requests_html-0.10.0-py3-none-any.whl", hash = "sha256:cb8a78cf829c4eca9d6233f28524f65dd2bfaafb4bdbbc407f0a0b8f487df6e2"}, +] + [[package]] name = "ruff" -version = "0.8.1" +version = "0.9.7" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev"] files = [ - {file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"}, - {file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"}, - {file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"}, - {file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"}, - {file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"}, - {file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"}, - {file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"}, + {file = "ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4"}, + {file = "ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66"}, + {file = "ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef"}, + {file = "ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0"}, + {file = "ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606"}, + {file = "ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d"}, + {file = "ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c"}, + {file = "ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037"}, + {file = "ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6"}, ] [[package]] @@ -869,17 +1244,28 @@ files = [ [[package]] name = "sentry-sdk" -version = "2.19.0" +version = "2.22.0" requires_python = ">=3.6" summary = "Python client for Sentry (https://sentry.io)" -groups = ["sentry"] +groups = ["default"] dependencies = [ "certifi", "urllib3>=1.26.11", ] files = [ - {file = "sentry_sdk-2.19.0-py2.py3-none-any.whl", hash = "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b"}, - {file = "sentry_sdk-2.19.0.tar.gz", hash = "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36"}, + {file = "sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66"}, + {file = "sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] @@ -904,12 +1290,71 @@ files = [ {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +requires_python = ">=3.9" +summary = "A lil' TOML writer" +groups = ["default"] +marker = "python_version >= \"3.11\"" +files = [ + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + +[[package]] +name = "tortoise-orm" +version = "0.24.0" +requires_python = "<4.0,>=3.9" +summary = "Easy async ORM for python, built with relations in mind" +groups = ["default"] +dependencies = [ + "aiosqlite<0.21.0,>=0.16.0", + "iso8601<3.0.0,>=2.1.0", + "pypika-tortoise<0.6.0,>=0.5.0", + "pytz", +] +files = [ + {file = "tortoise_orm-0.24.0-py3-none-any.whl", hash = "sha256:ee3b72b226767293b24c5c4906ae5f027d7cc84496cd503352c918564b4fd687"}, + {file = "tortoise_orm-0.24.0.tar.gz", hash = "sha256:ae0704a93ea27931724fc899e57268c8081afce3b32b110b00037ec206553e7d"}, +] + +[[package]] +name = "tortoise-orm" +version = "0.24.0" +extras = ["asyncpg"] +requires_python = "<4.0,>=3.9" +summary = "Easy async ORM for python, built with relations in mind" +groups = ["default"] +dependencies = [ + "asyncpg", + "tortoise-orm==0.24.0", +] +files = [ + {file = "tortoise_orm-0.24.0-py3-none-any.whl", hash = "sha256:ee3b72b226767293b24c5c4906ae5f027d7cc84496cd503352c918564b4fd687"}, + {file = "tortoise_orm-0.24.0.tar.gz", hash = "sha256:ae0704a93ea27931724fc899e57268c8081afce3b32b110b00037ec206553e7d"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +requires_python = ">=3.7" +summary = "Fast, Extensible Progress Meter" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default"] +groups = ["default", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -917,35 +1362,46 @@ files = [ [[package]] name = "urllib3" -version = "2.2.3" -requires_python = ">=3.8" +version = "2.3.0" +requires_python = ">=3.9" summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default", "sentry"] +groups = ["default"] +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[[package]] +name = "w3lib" +version = "2.3.1" +requires_python = ">=3.9" +summary = "Library of web-related functions" +groups = ["default"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "w3lib-2.3.1-py3-none-any.whl", hash = "sha256:9ccd2ae10c8c41c7279cd8ad4fe65f834be894fe7bfdd7304b991fd69325847b"}, + {file = "w3lib-2.3.1.tar.gz", hash = "sha256:5c8ac02a3027576174c2b61eb9a2170ba1b197cae767080771b6f1febda249a4"}, ] [[package]] name = "websockets" -version = "13.1" -requires_python = ">=3.8" +version = "15.0" +requires_python = ">=3.9" summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -groups = ["dev"] +groups = ["default", "dev"] files = [ - {file = "websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19"}, - {file = "websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5"}, - {file = "websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7"}, - {file = "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"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9"}, - {file = "websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f"}, - {file = "websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557"}, - {file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, - {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, + {file = "websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f"}, + {file = "websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d"}, + {file = "websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276"}, + {file = "websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc"}, + {file = "websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72"}, + {file = "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"}, + {file = "websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab"}, + {file = "websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99"}, + {file = "websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc"}, + {file = "websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904"}, + {file = "websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa"}, + {file = "websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3"}, + {file = "websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab"}, ] [[package]] @@ -964,22 +1420,24 @@ files = [ [[package]] name = "wrapt" -version = "1.17.0" +version = "1.17.2" requires_python = ">=3.8" summary = "Module for decorators, wrappers and monkey patching." groups = ["dev"] files = [ - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, ] [[package]] @@ -998,7 +1456,7 @@ files = [ [[package]] name = "yarl" -version = "1.18.0" +version = "1.18.3" requires_python = ">=3.9" summary = "Yet another URL library" groups = ["default"] @@ -1008,22 +1466,22 @@ dependencies = [ "propcache>=0.2.0", ] files = [ - {file = "yarl-1.18.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b8e8c516dc4e1a51d86ac975b0350735007e554c962281c432eaa5822aa9765c"}, - {file = "yarl-1.18.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6b4466714a73f5251d84b471475850954f1fa6acce4d3f404da1d55d644c34"}, - {file = "yarl-1.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c893f8c1a6d48b25961e00922724732d00b39de8bb0b451307482dc87bddcd74"}, - {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13aaf2bdbc8c86ddce48626b15f4987f22e80d898818d735b20bd58f17292ee8"}, - {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd21c0128e301851de51bc607b0a6da50e82dc34e9601f4b508d08cc89ee7929"}, - {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205de377bd23365cd85562c9c6c33844050a93661640fda38e0567d2826b50df"}, - {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed69af4fe2a0949b1ea1d012bf065c77b4c7822bad4737f17807af2adb15a73c"}, - {file = "yarl-1.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e1c18890091aa3cc8a77967943476b729dc2016f4cfe11e45d89b12519d4a93"}, - {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91b8fb9427e33f83ca2ba9501221ffaac1ecf0407f758c4d2f283c523da185ee"}, - {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:536a7a8a53b75b2e98ff96edb2dfb91a26b81c4fed82782035767db5a465be46"}, - {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a64619a9c47c25582190af38e9eb382279ad42e1f06034f14d794670796016c0"}, - {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c73a6bbc97ba1b5a0c3c992ae93d721c395bdbb120492759b94cc1ac71bc6350"}, - {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a173401d7821a2a81c7b47d4e7d5c4021375a1441af0c58611c1957445055056"}, - {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7520e799b1f84e095cce919bd6c23c9d49472deeef25fe1ef960b04cca51c3fc"}, - {file = "yarl-1.18.0-cp311-cp311-win32.whl", hash = "sha256:c4cb992d8090d5ae5f7afa6754d7211c578be0c45f54d3d94f7781c495d56716"}, - {file = "yarl-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:52c136f348605974c9b1c878addd6b7a60e3bf2245833e370862009b86fa4689"}, - {file = "yarl-1.18.0-py3-none-any.whl", hash = "sha256:dbf53db46f7cf176ee01d8d98c39381440776fcda13779d269a8ba664f69bec0"}, - {file = "yarl-1.18.0.tar.gz", hash = "sha256:20d95535e7d833889982bfe7cc321b7f63bf8879788fee982c76ae2b24cfb715"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, ] diff --git a/pyproject.toml b/pyproject.toml index 35ff31a..1c9e68a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "aiohttp>=3.9.5", "pyyaml>=6.0.1", "python-dotenv>=1.0.1", - "orjson>=3.10.5", + "orjson>=3.10.7", "pytz>=2024.1", "typing-extensions>=4.12.2", "schema>=0.7.7", @@ -18,22 +18,23 @@ dependencies = [ "quart>=0.19.6", "pydantic>=2.9.2", "coloredlogs>=15.0.1", + "aiofile>=3.9.0", + "sentry-sdk>=2.18.0", + "aiocache[redis]>=0.12.3", + "tortoise-orm[asyncpg]>=0.23.0", + "aerich[toml]>=0.8.1", "pypartpicker>=1.9.5", - "aiocache>=0.12.3", "aiolimiter>=1.1.0", ] -requires-python = "==3.11.*" +requires-python = "==3.12.*" readme = "README.md" license = {text = "MIT"} -[project.optional-dependencies] -sentry = [ - "sentry-sdk>=2.15.0", -] + [tool.pdm.scripts] format = "ruff format ." lint = "ruff check --fix ." -export = "pdm export -o requirements.txt --without-hashes --prod" +export = "pdm export -o requirements.txt --prod" tests = "pytest tests" start = "python src" check-listings = {call = "scripts:check_listings.main"} @@ -53,7 +54,64 @@ dev = [ ] [tool.pyright] +venvPath = "." +venv = ".venv" reportAny = false reportUnusedCallResult = false reportUnknownMemberType = false -pythonVersion = "3.11" +reportMissingTypeStubs = false +pythonVersion = "3.12" + +[tool.ruff] +target-version = "py312" +line-length = 120 +indent-width = 4 + +[tool.ruff.format] +quote-style = "double" + +indent-style = "space" + +skip-magic-trailing-comma = false + +line-ending = "auto" + +docstring-code-format = false + +docstring-code-line-length = "dynamic" + +[tool.ruff.lint] +select = ["ALL"] +per-file-ignores = { "src/database/migrations/*"= ["INP001", "ARG001"] } +extend-ignore = [ + "N999", + "D104", + "D100", + "D103", + "D102", + "D101", + "D107", + "D105", + "D106", + "ANN401", + "TRY003", + "EM101", + "EM102", + "G004", + "PTH", + "D211", + "D213", + "COM812", + "ISC001", + "D203", + "FBT001", + "FBT002", + "PLR2004", + "PLR0913", + "C901" +] + +[tool.aerich] +tortoise_orm = "src.database.config.TORTOISE_ORM" +location = "./src/database/migrations" +src_folder = "./." diff --git a/renovate.json b/renovate.json index 9da65f3..58f3302 100644 --- a/renovate.json +++ b/renovate.json @@ -1,31 +1,19 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ], - "baseBranches": [ - "dev" - ], - "labels": [ - "deps" - ], - "ignorePaths": [ - "requirements.txt" - ], + "extends": ["config:recommended"], + "baseBranches": ["dev"], + "labels": ["deps"], + "ignorePaths": ["requirements.txt"], "commitMessagePrefix": "⬆️", "commitMessageAction": "Upgrade", "packageRules": [ { - "updateTypes": [ - "pin" - ], + "updateTypes": ["pin"], "commitMessagePrefix": "📌", "commitMessageAction": "Pin" }, { - "updateTypes": [ - "rollback" - ], + "updateTypes": ["rollback"], "commitMessagePrefix": "⬇️", "commitMessageAction": "Downgrade" }, @@ -35,6 +23,3 @@ } ] } - - - diff --git a/requirements.txt b/requirements.txt index 642e390..91f708b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,52 +1,402 @@ # This file is @generated by PDM. # Please do not edit it manually. -aiocache==0.12.3 -aiofiles==24.1.0 -aiohappyeyeballs==2.4.4 -aiohttp==3.11.8 -aiolimiter==1.1.1 -aiosignal==1.3.1 -annotated-types==0.7.0 -attrs==24.2.0 -beautifulsoup4==4.12.3 -blinker==1.9.0 -bs4==0.0.2 -certifi==2024.8.30 -charset-normalizer==3.4.0 -click==8.1.7 -colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" -coloredlogs==15.0.1 -flask==3.1.0 -frozenlist==1.5.0 -h11==0.14.0 -h2==4.1.0 -hpack==4.0.0 -humanfriendly==10.0 -hypercorn==0.17.3 -hyperframe==6.0.1 -idna==3.10 -itsdangerous==2.2.0 -jinja2==3.1.4 -markupsafe==3.0.2 -multidict==6.1.0 -orjson==3.10.12 -priority==2.0.0 -propcache==0.2.0 -py-cord==2.6.1 -pydantic==2.10.2 -pydantic-core==2.27.1 -pypartpicker==1.9.5 -pyreadline3==3.5.4; sys_platform == "win32" and python_version >= "3.8" -python-dotenv==1.0.1 -pytz==2024.2 -pyyaml==6.0.2 -quart==0.19.9 -requests==2.32.3 -schema==0.7.7 -soupsieve==2.6 -typing-extensions==4.12.2 -urllib3==2.2.3 -werkzeug==3.1.3 -wsproto==1.2.0 -yarl==1.18.0 +aerich[toml]==0.8.1 \ + --hash=sha256:1e95b1c04dfc0c634dd43b0123933038c820140e17a4b27885a63b7461eb0632 \ + --hash=sha256:2743cf85bd9957ea173055dad07ee5a3219067e4f117d5402a44204c27e83c9f +aiocache[redis]==0.12.3 \ + --hash=sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d \ + --hash=sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713 +aiofile==3.9.0 \ + --hash=sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa \ + --hash=sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b +aiofiles==24.1.0 \ + --hash=sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c \ + --hash=sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5 +aiohappyeyeballs==2.4.6 \ + --hash=sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1 \ + --hash=sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0 +aiohttp==3.11.12 \ + --hash=sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a \ + --hash=sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0 \ + --hash=sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802 \ + --hash=sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef \ + --hash=sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e \ + --hash=sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0 \ + --hash=sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1 \ + --hash=sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259 \ + --hash=sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0 \ + --hash=sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9 \ + --hash=sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9 \ + --hash=sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f \ + --hash=sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9 \ + --hash=sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df \ + --hash=sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c \ + --hash=sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d \ + --hash=sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250 +aiolimiter==1.2.1 \ + --hash=sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7 \ + --hash=sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9 +aiosignal==1.3.2 \ + --hash=sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5 \ + --hash=sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54 +aiosqlite==0.20.0 \ + --hash=sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6 \ + --hash=sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7 +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +anyio==4.8.0 \ + --hash=sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a \ + --hash=sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 +asyncclick==8.1.8 \ + --hash=sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c \ + --hash=sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678 \ + --hash=sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6 +asyncpg==0.30.0 \ + --hash=sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a \ + --hash=sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737 \ + --hash=sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e \ + --hash=sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3 \ + --hash=sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305 \ + --hash=sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a \ + --hash=sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851 \ + --hash=sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e \ + --hash=sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af +attrs==25.1.0 \ + --hash=sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e \ + --hash=sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a +beautifulsoup4==4.13.3 \ + --hash=sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b \ + --hash=sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16 +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc +bs4==0.0.2 \ + --hash=sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925 \ + --hash=sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc +caio==0.9.21 \ + --hash=sha256:1f4241e2b89f31e1fea342c8da9a987fef71d093df7ba1169f469d0be7592608 \ + --hash=sha256:4f1d30ad0f975de07b4a3ae1cd2e9275fa574f2ca0b49ba5ab16208575650a92 \ + --hash=sha256:e7314a28d69a0397d1d0ac6c7bfe7973f27f4f7216cf42b0358d7d9ba27bc4cd \ + --hash=sha256:fdab8c817b6835d997db1532ce7d9f5cbe186265bf0ee9a9840b378aa4a1cba7 +certifi==2025.1.31 \ + --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ + --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe +charset-normalizer==3.4.1 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 +click==8.1.8 \ + --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ + --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a +colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +coloredlogs==15.0.1 \ + --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ + --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 +cssselect==1.2.0 \ + --hash=sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc \ + --hash=sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e +dictdiffer==0.9.0 \ + --hash=sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578 \ + --hash=sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595 +fake-useragent==2.0.3 \ + --hash=sha256:8bae50abb72c309a5b3ae2f01a0b82426613fd5c4e2a04dca9332399ec44daa1 \ + --hash=sha256:af86a26ef8229efece8fed529b4aeb5b73747d889b60f01cd477b6f301df46e6 +flask==3.1.0 \ + --hash=sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac \ + --hash=sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136 +frozenlist==1.5.0 \ + --hash=sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e \ + --hash=sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8 \ + --hash=sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6 \ + --hash=sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21 \ + --hash=sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f \ + --hash=sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9 \ + --hash=sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a \ + --hash=sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784 \ + --hash=sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d \ + --hash=sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e \ + --hash=sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee \ + --hash=sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817 \ + --hash=sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039 \ + --hash=sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f \ + --hash=sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631 \ + --hash=sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3 \ + --hash=sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 +h2==4.2.0 \ + --hash=sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0 \ + --hash=sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f +hpack==4.1.0 \ + --hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 \ + --hash=sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca +humanfriendly==10.0 \ + --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ + --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc +hypercorn==0.17.3 \ + --hash=sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547 \ + --hash=sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165 +hyperframe==6.1.0 \ + --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \ + --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08 +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +iso8601==2.1.0 \ + --hash=sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df \ + --hash=sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242 +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 +jinja2==3.1.5 \ + --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ + --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb +lxml-html-clean==0.4.1 \ + --hash=sha256:40c838bbcf1fc72ba4ce811fbb3135913017b27820d7c16e8bc412ae1d8bc00b \ + --hash=sha256:b704f2757e61d793b1c08bf5ad69e4c0b68d6696f4c3c1429982caf90050bcaf +lxml[html-clean]==5.3.1 \ + --hash=sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8 \ + --hash=sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae \ + --hash=sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf \ + --hash=sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f \ + --hash=sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0 \ + --hash=sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468 \ + --hash=sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe \ + --hash=sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7 \ + --hash=sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519 \ + --hash=sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c \ + --hash=sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9 \ + --hash=sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322 \ + --hash=sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd \ + --hash=sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a \ + --hash=sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367 \ + --hash=sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645 \ + --hash=sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c \ + --hash=sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5 +markupsafe==3.0.2 \ + --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ + --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ + --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ + --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ + --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ + --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ + --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ + --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ + --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ + --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 +multidict==6.1.0 \ + --hash=sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761 \ + --hash=sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6 \ + --hash=sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966 \ + --hash=sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1 \ + --hash=sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305 \ + --hash=sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a \ + --hash=sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3 \ + --hash=sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506 \ + --hash=sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925 \ + --hash=sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e \ + --hash=sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95 \ + --hash=sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133 \ + --hash=sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436 \ + --hash=sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2 \ + --hash=sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2 \ + --hash=sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa \ + --hash=sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef +orjson==3.10.15 \ + --hash=sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514 \ + --hash=sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e \ + --hash=sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665 \ + --hash=sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4 \ + --hash=sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b \ + --hash=sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0 \ + --hash=sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7 \ + --hash=sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a \ + --hash=sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a \ + --hash=sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41 \ + --hash=sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17 \ + --hash=sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767 \ + --hash=sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d \ + --hash=sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa +parse==1.20.2 \ + --hash=sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558 \ + --hash=sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce +priority==2.0.0 \ + --hash=sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa \ + --hash=sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0 +propcache==0.3.0 \ + --hash=sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57 \ + --hash=sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64 \ + --hash=sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e \ + --hash=sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b \ + --hash=sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043 \ + --hash=sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d \ + --hash=sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138 \ + --hash=sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c \ + --hash=sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7 \ + --hash=sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7 \ + --hash=sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667 \ + --hash=sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86 \ + --hash=sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af \ + --hash=sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5 \ + --hash=sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf \ + --hash=sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d \ + --hash=sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e \ + --hash=sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5 +py-cord==2.6.1 \ + --hash=sha256:36064f225f2c7bbddfe542d5ed581f2a5744f618e039093cf7cd2659a58bc79b \ + --hash=sha256:e3d3b528c5e37b0e0825f5b884cbb9267860976c1e4878e28b55da8fd3af834b +pydantic==2.10.6 \ + --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ + --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic-core==2.27.2 \ + --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ + --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ + --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ + --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ + --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ + --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ + --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ + --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ + --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ + --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ + --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ + --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ + --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ + --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ + --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 +pyee==12.1.1 \ + --hash=sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef \ + --hash=sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3 +pypartpicker==2.0.5 \ + --hash=sha256:27c30e50d0f581bdef82c3966901ae9cacc15c9f0748a480dc70a56feb936bde \ + --hash=sha256:738fbec9f0dc1226fd6926694b8f189f20bd794d2473765aa4205e7f6015b2c3 +pypika-tortoise==0.5.0 \ + --hash=sha256:dbdc47eb52ce17407b05ce9f8560ce93b856d7b28beb01971d956b017846691f \ + --hash=sha256:ed0f56761868dc222c03e477578638590b972280b03c7c45cd93345b18b61f58 +pyppeteer==0.0.25 \ + --hash=sha256:51fe769b722a1718043b74d12c20420f29e0dd9eeea2b66652b7f93a9ad465dd +pyquery==2.0.1 \ + --hash=sha256:0194bb2706b12d037db12c51928fe9ebb36b72d9e719565daba5a6c595322faf \ + --hash=sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0 +pyreadline3==3.5.4; sys_platform == "win32" and python_version >= "3.8" \ + --hash=sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7 \ + --hash=sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6 +python-dotenv==1.0.1 \ + --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ + --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a +pytz==2025.1 \ + --hash=sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57 \ + --hash=sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e +pyyaml==6.0.2 \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 +quart==0.20.0 \ + --hash=sha256:003c08f551746710acb757de49d9b768986fd431517d0eb127380b656b98b8f1 \ + --hash=sha256:08793c206ff832483586f5ae47018c7e40bdd75d886fee3fabbdaa70c2cf505d +redis==5.2.1 \ + --hash=sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f \ + --hash=sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4 +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 +requests-html==0.10.0 \ + --hash=sha256:7e929ecfed95fb1d0994bb368295d6d7c4d06b03fcb900c33d7d0b17e6003947 \ + --hash=sha256:cb8a78cf829c4eca9d6233f28524f65dd2bfaafb4bdbbc407f0a0b8f487df6e2 +schema==0.7.7 \ + --hash=sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde \ + --hash=sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807 +sentry-sdk==2.22.0 \ + --hash=sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66 \ + --hash=sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944 +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc +soupsieve==2.6 \ + --hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \ + --hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9 +tomli-w==1.2.0; python_version >= "3.11" \ + --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ + --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 +tortoise-orm[asyncpg]==0.24.0 \ + --hash=sha256:ae0704a93ea27931724fc899e57268c8081afce3b32b110b00037ec206553e7d \ + --hash=sha256:ee3b72b226767293b24c5c4906ae5f027d7cc84496cd503352c918564b4fd687 +tqdm==4.67.1 \ + --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ + --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d +w3lib==2.3.1 \ + --hash=sha256:5c8ac02a3027576174c2b61eb9a2170ba1b197cae767080771b6f1febda249a4 \ + --hash=sha256:9ccd2ae10c8c41c7279cd8ad4fe65f834be894fe7bfdd7304b991fd69325847b +websockets==15.0 \ + --hash=sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab \ + --hash=sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276 \ + --hash=sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904 \ + --hash=sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3 \ + --hash=sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d \ + --hash=sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc \ + --hash=sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc \ + --hash=sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99 \ + --hash=sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72 \ + --hash=sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab \ + --hash=sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f \ + --hash=sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d \ + --hash=sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa +werkzeug==3.1.3 \ + --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ + --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 +wsproto==1.2.0 \ + --hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \ + --hash=sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736 +yarl==1.18.3 \ + --hash=sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba \ + --hash=sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50 \ + --hash=sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640 \ + --hash=sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2 \ + --hash=sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393 \ + --hash=sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272 \ + --hash=sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576 \ + --hash=sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477 \ + --hash=sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512 \ + --hash=sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1 \ + --hash=sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b \ + --hash=sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e \ + --hash=sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb \ + --hash=sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6 \ + --hash=sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285 \ + --hash=sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb \ + --hash=sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75 \ + --hash=sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2 diff --git a/scripts/check_listings/__main__.py b/scripts/check_listings/__main__.py index d429f9a..a65a5d9 100644 --- a/scripts/check_listings/__main__.py +++ b/scripts/check_listings/__main__.py @@ -1,39 +1,39 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT +import argparse import asyncio -import nodriver as uc +from typing import Any + import markdown -import argparse +import nodriver as uc import yaml - -from termcolor import cprint +from aiofile import async_open as open # noqa: A004 from bs4 import BeautifulSoup +from termcolor import cprint from .listings import ( - normalize_soup, - TopGg, - DiscordsCom, - WumpusStore, DiscordAppDirectory, DiscordBotListCom, - DisforgeCom, DiscordBotsGg, DiscordMe, + DiscordsCom, + DisforgeCom, NotFoundError, + TopGg, + WumpusStore, + normalize_soup, ) -COMPLETED = False +completed = False -async def async_main(args): - with open("description.md", "r", encoding="utf-8") as f: - description: str = f.read() - with open(args.config, "r", encoding="utf-8") as f: - config: dict = yaml.safe_load(f) - application_id = ( - args.application_id if args.application_id else config["application_id"] - ) +async def async_main(args: argparse.Namespace) -> None: + async with open("description.md", encoding="utf-8") as f: + description: str = await f.read() + async with open(args.config, encoding="utf-8") as f: + config: dict[Any, Any] = yaml.safe_load(await f.read()) + application_id = args.application_id if args.application_id else config["application_id"] description = markdown.markdown(description) description = normalize_soup(BeautifulSoup(description, "html.parser")) @@ -62,18 +62,18 @@ async def async_main(args): except NotFoundError: cprint(f"{listing.name} not published", "black", "on_light_red") continue - except asyncio.TimeoutError: + except TimeoutError: cprint(f"{listing.name} timed out") continue if description == its_description: cprint(f"{listing.name} matches", "black", "on_green") else: cprint(f"{listing.name} does not match", "black", "on_yellow") - global COMPLETED - COMPLETED = True + global completed # noqa: PLW0603 + completed = True -def main(): +def main() -> None: parser = argparse.ArgumentParser( prog="Listings checker", description="Check the published status of your discord listings", @@ -84,8 +84,8 @@ def main(): args = parser.parse_args() try: asyncio.get_event_loop().run_until_complete(async_main(args)) - except Exception as e: # noqa - if not COMPLETED: + except Exception: + if not completed: raise diff --git a/scripts/check_listings/listings/DiscordAppDirectory.py b/scripts/check_listings/listings/DiscordAppDirectory.py index 3736bc8..771c83d 100644 --- a/scripts/check_listings/listings/DiscordAppDirectory.py +++ b/scripts/check_listings/listings/DiscordAppDirectory.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import nodriver as uc - from bs4 import BeautifulSoup from .Listing import Listing @@ -11,11 +10,11 @@ class DiscordAppDirectory(Listing): name: str = "Discord App Directory" - def __init__(self, browser: uc.Browser, application_id: int): + def __init__(self, browser: uc.Browser, application_id: int) -> None: super().__init__(browser) self.application_id = application_id - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: url = f"https://discord.com/application-directory/{self.application_id}" page = await self.browser.get(url) description = await page.select(".detailedDescription_a1eac2", timeout=25) diff --git a/scripts/check_listings/listings/DiscordBotListCom.py b/scripts/check_listings/listings/DiscordBotListCom.py index 1de1474..7263d45 100644 --- a/scripts/check_listings/listings/DiscordBotListCom.py +++ b/scripts/check_listings/listings/DiscordBotListCom.py @@ -1,9 +1,8 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import nodriver as uc -import asyncio +import nodriver as uc from bs4 import BeautifulSoup from .Listing import Listing, NotFoundError @@ -12,16 +11,16 @@ class DiscordBotListCom(Listing): name: str = "DiscordBotList.com" - def __init__(self, browser: uc.Browser, url: str): + def __init__(self, browser: uc.Browser, url: str) -> None: super().__init__(browser) self.url = url - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: page = await self.browser.get(self.url) try: await page.find("Page not found") raise NotFoundError("Listing not found") - except asyncio.TimeoutError: + except TimeoutError: pass description = await page.select("article > .markdown") html = await description.get_html() diff --git a/scripts/check_listings/listings/DiscordBotsGg.py b/scripts/check_listings/listings/DiscordBotsGg.py index 8d4c081..19bc813 100644 --- a/scripts/check_listings/listings/DiscordBotsGg.py +++ b/scripts/check_listings/listings/DiscordBotsGg.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import nodriver as uc - from bs4 import BeautifulSoup from .Listing import Listing, NotFoundError @@ -11,18 +10,18 @@ class DiscordBotsGg(Listing): name: str = "Discord.bots.gg" - def __init__(self, browser: uc.Browser, application_id: int): + def __init__(self, browser: uc.Browser, application_id: int) -> None: super().__init__(browser) self.application_id = application_id - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: url = f"https://discord.bots.gg/bots/{self.application_id}" page = await self.browser.get(url) if ( len( await page.query_selector_all( ".error__title", - ) + ), ) != 0 ): @@ -30,4 +29,4 @@ async def fetch_raw_description(self): description = await page.select(".bot__description") html = await description.get_html() soup = BeautifulSoup(html, "html.parser") - return self.normalize_soup(soup).replace("’", "'") + return self.normalize_soup(soup).replace("’", "'") # noqa: RUF001 diff --git a/scripts/check_listings/listings/DiscordMe.py b/scripts/check_listings/listings/DiscordMe.py index 61869fb..9dea539 100644 --- a/scripts/check_listings/listings/DiscordMe.py +++ b/scripts/check_listings/listings/DiscordMe.py @@ -1,9 +1,8 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import nodriver as uc -import asyncio +import nodriver as uc from bs4 import BeautifulSoup from .Listing import Listing, NotFoundError @@ -12,16 +11,16 @@ class DiscordMe(Listing): name: str = "Discord.me" - def __init__(self, browser: uc.Browser, url: str): + def __init__(self, browser: uc.Browser, url: str) -> None: super().__init__(browser) self.url = url - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: page = await self.browser.get(self.url) try: await page.find("Sorry, the page you are looking for could not be found.") raise NotFoundError("Listing not found") - except asyncio.TimeoutError: + except TimeoutError: pass description = await page.select(".server-sidebar > p") html = await description.get_html() diff --git a/scripts/check_listings/listings/DiscordsCom.py b/scripts/check_listings/listings/DiscordsCom.py index be38049..65d7cb1 100644 --- a/scripts/check_listings/listings/DiscordsCom.py +++ b/scripts/check_listings/listings/DiscordsCom.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import nodriver as uc - from bs4 import BeautifulSoup from .Listing import Listing, NotFoundError @@ -11,11 +10,11 @@ class DiscordsCom(Listing): name: str = "Discords.com" - def __init__(self, browser: uc.Browser, application_id: int): + def __init__(self, browser: uc.Browser, application_id: int) -> None: super().__init__(browser) self.application_id = application_id - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: url = f"https://discords.com/bots/bot/{self.application_id}" page = await self.browser.get(url) description = await page.select("app-bot-page-description") diff --git a/scripts/check_listings/listings/DisforgeCom.py b/scripts/check_listings/listings/DisforgeCom.py index dbbd805..4db5984 100644 --- a/scripts/check_listings/listings/DisforgeCom.py +++ b/scripts/check_listings/listings/DisforgeCom.py @@ -2,27 +2,24 @@ # SPDX-License-Identifier: MIT import nodriver as uc - from bs4 import BeautifulSoup from markdown import markdown + from .Listing import Listing, NotFoundError class DisforgeCom(Listing): name: str = "Disforge.com" - def __init__(self, browser: uc.Browser, url: str): + def __init__(self, browser: uc.Browser, url: str) -> None: super().__init__(browser) self.url = url - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: page = await self.browser.get(self.url) await page.wait(4) # if the window location is homepage, then the bot is not found - if ( - len(await page.find_elements_by_text("Vote for this bot", tag_hint="a")) - == 0 - ): + if len(await page.find_elements_by_text("Vote for this bot", tag_hint="a")) == 0: raise NotFoundError("Listing not found") description = await page.select(".card-body") html = await description.get_html() diff --git a/scripts/check_listings/listings/Listing.py b/scripts/check_listings/listings/Listing.py index 37c2f9c..ac47e3c 100644 --- a/scripts/check_listings/listings/Listing.py +++ b/scripts/check_listings/listings/Listing.py @@ -1,9 +1,9 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import nodriver as uc - from abc import ABC, abstractmethod + +import nodriver as uc from bs4 import BeautifulSoup @@ -16,32 +16,24 @@ class NotFoundError(BaseError): def normalize_soup(soup: BeautifulSoup) -> str: - """ - Normalize the text from a BeautifulSoup object - """ - return soup.get_text().strip().replace("’", "'").replace("\n", "") + """Normalize the text from a BeautifulSoup object.""" + return soup.get_text().strip().replace("’", "'").replace("\n", "") # noqa: RUF001 class Listing(ABC): - """ - Represents a Discord Bot listing website - """ + """Represents a Discord Bot listing website.""" - def __init__(self, browser: uc.Browser, *args, **kwargs): + def __init__(self, browser: uc.Browser) -> None: self.browser = browser def normalize_soup(self, soup: BeautifulSoup) -> str: - """ - Normalize the text from a BeautifulSoup object - """ + """Normalize the text from a BeautifulSoup object.""" return normalize_soup(soup) @abstractmethod async def fetch_raw_description(self) -> str: - """ - Fetch the raw description of the bot from the website + """Fetch the raw description of the bot from the website. :raises NotFoundError: If the bot is not found :return: The raw description of the bot """ - pass diff --git a/scripts/check_listings/listings/TopGg.py b/scripts/check_listings/listings/TopGg.py index 269b448..f1454c4 100644 --- a/scripts/check_listings/listings/TopGg.py +++ b/scripts/check_listings/listings/TopGg.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import nodriver as uc - from bs4 import BeautifulSoup from .Listing import Listing, NotFoundError @@ -11,19 +10,19 @@ class TopGg(Listing): name: str = "Top.gg" - def __init__(self, browser: uc.Browser, application_id: int): + def __init__(self, browser: uc.Browser, application_id: int) -> None: super().__init__(browser) self.application_id = application_id - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: url = f"https://top.gg/bot/{self.application_id}" page = await self.browser.get(url) if ( len( await page.find_elements_by_text( - "Oops! We can’t seem to find the page you’re looking for.", + "Oops! We can’t seem to find the page you’re looking for.", # noqa: RUF001 tag_hint="p", - ) + ), ) != 0 ): diff --git a/scripts/check_listings/listings/WumpusStore.py b/scripts/check_listings/listings/WumpusStore.py index 344d732..41cbe96 100644 --- a/scripts/check_listings/listings/WumpusStore.py +++ b/scripts/check_listings/listings/WumpusStore.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import nodriver as uc - from bs4 import BeautifulSoup from .Listing import Listing @@ -11,11 +10,11 @@ class WumpusStore(Listing): name: str = "Wumpus.store" - def __init__(self, browser: uc.Browser, application_id: int): + def __init__(self, browser: uc.Browser, application_id: int) -> None: super().__init__(browser) self.application_id = application_id - async def fetch_raw_description(self): + async def fetch_raw_description(self) -> str: url = f"https://wumpus.store/bot/{self.application_id}" page = await self.browser.get(url) description = await page.select(".css-2yutdr") diff --git a/scripts/check_listings/listings/__init__.py b/scripts/check_listings/listings/__init__.py index c1ccfd8..6cf16e9 100644 --- a/scripts/check_listings/listings/__init__.py +++ b/scripts/check_listings/listings/__init__.py @@ -1,26 +1,27 @@ # Copyright (c) NiceBots # SPDX-License-Identifier: MIT -from .TopGg import TopGg -from .DiscordsCom import DiscordsCom -from .Listing import BaseError, NotFoundError, normalize_soup -from .WumpusStore import WumpusStore from .DiscordAppDirectory import DiscordAppDirectory from .DiscordBotListCom import DiscordBotListCom -from .DisforgeCom import DisforgeCom from .DiscordBotsGg import DiscordBotsGg from .DiscordMe import DiscordMe +from .DiscordsCom import DiscordsCom +from .DisforgeCom import DisforgeCom +from .Listing import BaseError, Listing, NotFoundError, normalize_soup +from .TopGg import TopGg +from .WumpusStore import WumpusStore __all__ = [ - "TopGg", - "DiscordsCom", "BaseError", - "NotFoundError", - "normalize_soup", - "WumpusStore", "DiscordAppDirectory", "DiscordBotListCom", - "DisforgeCom", "DiscordBotsGg", "DiscordMe", + "DiscordsCom", + "DisforgeCom", + "Listing", + "NotFoundError", + "TopGg", + "WumpusStore", + "normalize_soup", ] diff --git a/scripts/check_listings/main.py b/scripts/check_listings/main.py index d429f9a..392c5ac 100644 --- a/scripts/check_listings/main.py +++ b/scripts/check_listings/main.py @@ -1,45 +1,45 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT +import argparse import asyncio -import nodriver as uc + import markdown -import argparse +import nodriver as uc import yaml - -from termcolor import cprint +from aiofile import async_open as open # noqa: A004 from bs4 import BeautifulSoup +from termcolor import cprint from .listings import ( - normalize_soup, - TopGg, - DiscordsCom, - WumpusStore, DiscordAppDirectory, DiscordBotListCom, - DisforgeCom, DiscordBotsGg, DiscordMe, + DiscordsCom, + DisforgeCom, + Listing, NotFoundError, + TopGg, + WumpusStore, + normalize_soup, ) -COMPLETED = False +completed = False -async def async_main(args): - with open("description.md", "r", encoding="utf-8") as f: - description: str = f.read() - with open(args.config, "r", encoding="utf-8") as f: - config: dict = yaml.safe_load(f) - application_id = ( - args.application_id if args.application_id else config["application_id"] - ) +async def async_main(args: argparse.Namespace) -> None: + async with open("description.md", encoding="utf-8") as f: + description: str = await f.read() + async with open(args.config, encoding="utf-8") as f: + config: dict[str, str] = yaml.safe_load(await f.read()) + application_id = args.application_id if args.application_id else config["application_id"] description = markdown.markdown(description) description = normalize_soup(BeautifulSoup(description, "html.parser")) browser = await uc.start() - listings = [ + listings: list[Listing] = [ DiscordsCom(browser, application_id), WumpusStore(browser, application_id), DiscordAppDirectory(browser, application_id), @@ -62,18 +62,18 @@ async def async_main(args): except NotFoundError: cprint(f"{listing.name} not published", "black", "on_light_red") continue - except asyncio.TimeoutError: + except TimeoutError: cprint(f"{listing.name} timed out") continue if description == its_description: cprint(f"{listing.name} matches", "black", "on_green") else: cprint(f"{listing.name} does not match", "black", "on_yellow") - global COMPLETED - COMPLETED = True + global completed # noqa: PLW0603 + completed = True -def main(): +def main() -> None: parser = argparse.ArgumentParser( prog="Listings checker", description="Check the published status of your discord listings", @@ -84,8 +84,8 @@ def main(): args = parser.parse_args() try: asyncio.get_event_loop().run_until_complete(async_main(args)) - except Exception as e: # noqa - if not COMPLETED: + except Exception: + if not completed: raise diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..8e05d9a --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# Copyright (c) NiceBots all rights reserved diff --git a/src/__main__.py b/src/__main__.py index 1762534..f845f62 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,45 +1,24 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import sys import os +import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) # noqa: E702 +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) # the above line allows us to import from src without any issues whilst using src/__main__.py -from src.config import config -import importlib.util import asyncio -from glob import iglob -from src.log import logger -from src.utils.setup_func import setup_func - - -async def load_and_run_patches(): - for patch_file in iglob("src/extensions/*/patch.py"): - extension = os.path.basename(os.path.dirname(patch_file)) - if config["extensions"].get(extension, {}).get("enabled", False): - logger.info(f"Loading patch for extension {extension}") - spec = importlib.util.spec_from_file_location( - f"src.extensions.{extension}.patch", patch_file - ) - if not spec or not spec.loader: - continue - patch_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(patch_module) - if hasattr(patch_module, "patch") and callable(patch_module.patch): - await setup_func( - patch_module.patch, config=config["extensions"][extension] - ) - - -async def pre_main(): + +from src.patcher import load_and_run_patches + + +async def main() -> None: await load_and_run_patches() - # we import main here to apply patches before importing the most things we can + # we import main here to apply patches before importing as many things we can # and allow the patches to be applied to later imported modules - from src.start import main + from src.start import start - await main() + await start() if __name__ == "__main__": - asyncio.run(pre_main()) + asyncio.run(main()) diff --git a/src/config/__init__.py b/src/config/__init__.py index 89b40d8..3085e44 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .bot_config import config, store_config +from .bot_config import config -__all__ = ["config", "store_config"] +__all__ = ["config"] diff --git a/src/config/bot_config.py b/src/config/bot_config.py index 00e2e59..f1aabe1 100644 --- a/src/config/bot_config.py +++ b/src/config/bot_config.py @@ -1,12 +1,13 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import yaml +import contextlib import os -import orjson +from typing import Any +import orjson +import yaml from dotenv import load_dotenv -from typing import Any load_dotenv() @@ -14,13 +15,13 @@ def load_from_env() -> dict[str, dict[str, Any]]: - _config = {} + _config: dict[str, Any] = {} values = {k: v for k, v in os.environ.items() if k.startswith(f"BOTKIT{SPLIT}")} values = {k[len(f"BOTKIT{SPLIT}") :]: v for k, v in values.items()} - current: dict = {} + current: dict[str, Any] = {} for key, value in values.items(): for i, part in enumerate(key.split(SPLIT)): - part = part.lower() + part = part.lower() # noqa: PLW2901 if i == 0: if part not in _config: _config[part] = {} @@ -47,10 +48,8 @@ def load_json_recursive(data: dict[str, Any]) -> dict[str, Any]: elif value.lower() == "false": data[key] = False else: - try: + with contextlib.suppress(orjson.JSONDecodeError): data[key] = orjson.loads(value) - except orjson.JSONDecodeError: - pass return data @@ -63,14 +62,7 @@ def load_json_recursive(data: dict[str, Any]) -> dict[str, Any]: config: dict[str, dict[str, Any]] if path: # noinspection PyArgumentEqualDefault - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: config = yaml.safe_load(f) else: config = load_from_env() - - -def store_config() -> None: - if path: - # noinspection PyShadowingNames - with open(path, "w", encoding="utf-8") as f: - yaml.dump(config, f) diff --git a/src/custom/__init__.py b/src/custom/__init__.py index 400b420..0e61d8c 100644 --- a/src/custom/__init__.py +++ b/src/custom/__init__.py @@ -1,61 +1,117 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from discord import Message -from discord.ext.bridge import BridgeExtContext # pyright: ignore [reportMissingTypeStubs] -from typing_extensions import override +import contextlib +from logging import getLogger +from typing import TYPE_CHECKING, Any, override + +import aiocache import discord -from src.i18n.classes import ExtensionTranslation, TranslationWrapper, apply_locale -from typing import Any, Union # pyright: ignore[reportDeprecated] +from discord import Interaction, Message, WebhookMessage from discord.ext import bridge -from logging import getLogger +from discord.ext.bridge import ( + BridgeExtContext, +) + +from src.i18n.classes import ExtensionTranslation, RawTranslation, TranslationWrapper, apply_locale + +if TYPE_CHECKING: + from src.database.models import Guild, User logger = getLogger("bot") class ApplicationContext(bridge.BridgeApplicationContext): - def __init__(self, bot: discord.Bot, interaction: discord.Interaction): - self.translations: TranslationWrapper = TranslationWrapper( + def __init__(self, bot: "Bot", interaction: discord.Interaction) -> None: + self.translations: TranslationWrapper[dict[str, RawTranslation]] = TranslationWrapper( {}, "en-US" ) # empty placeholder - super().__init__(bot=bot, interaction=interaction) # pyright: ignore[reportUnknownMemberType] + super().__init__(bot=bot, interaction=interaction) + self.bot: Bot + self.user_obj: User | None = None + self.guild_obj: Guild | None = None + self.custom_attrs: dict[str, Any] = {} @override - def __setattr__(self, key: Any, value: Any): - if key == "command": - if hasattr(value, "translations"): - self.translations = apply_locale( - value.translations, - self.locale, - ) + def __setattr__(self, key: Any, value: Any) -> None: + if key == "command" and hasattr(value, "translations"): + self.translations = apply_locale( + value.translations, + self.locale, + ) super().__setattr__(key, value) -class ExtContext(bridge.BridgeExtContext): - def __init__(self, **kwargs: Any): - self.translations: TranslationWrapper = TranslationWrapper( - {}, "en-US" - ) # empty placeholder - super().__init__(**kwargs) # pyright: ignore[reportUnknownMemberType] +async def remove_reaction(user: discord.User, message: discord.Message, emoji: str) -> None: + await message.remove_reaction(emoji, user) + - def load_translations(self): - if hasattr(self.command, "translations") and self.command.translations: # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType,reportOptionalMemberAccess,reportAttributeAccessIssue] +class ExtContext(bridge.BridgeExtContext): + def __init__(self, **kwargs: Any) -> None: + self.translations: TranslationWrapper = TranslationWrapper({}, "en-US") # empty placeholder + super().__init__(**kwargs) + self.bot: Bot + self.user_obj: User | None = None + self.guild_obj: Guild | None = None + self.custom_attrs: dict[str, Any] = {} + + def load_translations(self) -> None: + if hasattr(self.command, "translations") and self.command.translations: # pyright: ignore[reportUnknownArgumentType,reportOptionalMemberAccess,reportAttributeAccessIssue] locale: str | None = None - if guild := self.guild: # pyright: ignore[reportUnnecessaryComparison] # for some reason pyright thinks guild is function - locale = guild.preferred_locale # pyright: ignore[reportFunctionMemberAccess] + if guild := self.guild: + locale = guild.preferred_locale self.translations = apply_locale( - self.command.translations, # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType,reportAttributeAccessIssue,reportOptionalMemberAccess] + self.command.translations, # pyright: ignore [reportAttributeAccessIssue, reportOptionalMemberAccess, reportUnknownArgumentType] locale, ) + @override + async def defer(self, *args: Any, **kwargs: Any) -> None: + await super().defer(*args, **kwargs) + with contextlib.suppress(Exception): + await self.message.add_reaction("🔄") + + @override + async def respond(self, *args: Any, **kwargs: Any) -> "Interaction | WebhookMessage | Message": + r = await super().respond(*args, **kwargs) + with contextlib.suppress(Exception): + if self.me: + await remove_reaction(self.me, self.message, "🔄") + return r + class Bot(bridge.Bot): - def __init__(self, *args: Any, **options: Any): + def __init__( + self, *args: Any, cache_type: str = "memory", cache_config: dict[str, Any] | None = None, **options: Any + ) -> None: self.translations: list[ExtensionTranslation] = options.pop("translations", []) - super().__init__(*args, **options) # pyright: ignore[reportUnknownMemberType] + + self.botkit_cache: aiocache.BaseCache + # Initialize cache based on type and config + if cache_type == "redis": + if cache_config: + logger.info("Using Redis cache") + self.botkit_cache = aiocache.RedisCache( + endpoint=cache_config.get("host", "localhost"), + port=cache_config.get("port", 6379), + db=cache_config.get("db", 0), + password=cache_config.get("password"), + ssl=cache_config.get("ssl", False), + namespace="botkit", + ) + else: + logger.warning( + "Redis cache type specified but no configuration provided. Falling back to memory cache." + ) + self.botkit_cache = aiocache.SimpleMemoryCache(namespace="botkit") + else: + logger.info("Using memory cache") + self.botkit_cache = aiocache.SimpleMemoryCache(namespace="botkit") + + super().__init__(*args, **options) @self.listen(name="on_ready", once=True) - async def on_ready(): # pyright: ignore[reportUnusedFunction] + async def on_ready() -> None: # pyright: ignore[reportUnusedFunction] logger.success("Bot started successfully") # pyright: ignore[reportAttributeAccessIssue] @override @@ -65,7 +121,7 @@ async def get_application_context( cls: None | type[bridge.BridgeApplicationContext] = None, ) -> bridge.BridgeApplicationContext: cls = cls if cls is not None else ApplicationContext - return await super().get_application_context(interaction, cls=cls) # pyright: ignore [reportUnknownMemberType] + return await super().get_application_context(interaction, cls=cls) @override async def get_context( @@ -74,13 +130,58 @@ async def get_context( cls: None | type[bridge.BridgeExtContext] = None, ) -> BridgeExtContext: cls = cls if cls is not None else ExtContext - ctx = await super().get_context(message, cls=cls) # pyright: ignore [reportUnknownMemberType] + ctx = await super().get_context(message, cls=cls) if isinstance(ctx, ExtContext): ctx.load_translations() return ctx - -# if we used | we would sometimes need to use 'Context' instead of Context when type hinting bc else the interpreter will crash -Context = Union[ExtContext, ApplicationContext] # pyright: ignore[reportDeprecated] - -__all__ = ["Bot", "Context", "ExtContext", "ApplicationContext"] + @property + @override + def intents(self) -> discord.Intents: + """The intents configured for this connection or a copy of the intents if the bot is connected. + + Returns + ------- + :class:`Intents` + The intents configured for this Client. + + """ + # _connection._intents returns the intents themselves, _connection.intents returns a copy + # so if the bot is connected, we return a copy so that changes don't affect the connection + # if the bot is not connected, we return the actual intents so that the user can make changes + if self.ws is None: # pyright: ignore [reportUnnecessaryComparison] + return self._connection._intents # noqa: SLF001 # pyright: ignore [reportPrivateUsage] + return self._connection.intents + + @intents.setter + def intents(self, value: Any) -> None: # pyright: ignore [reportExplicitAny] + """Set the intents for this Client. + + Parameters + ---------- + value: :class:`Intents` + The intents to set for this Client. + + Raises + ------ + TypeError + The value is not an instance of Intents. + AttributeError + The intents cannot be changed after the connection is established. + + """ + if not isinstance(value, discord.Intents): + raise TypeError(f"Intents must be an instance of Intents not {value.__class__!r}") + if self.ws is not None: # pyright: ignore [reportUnnecessaryComparison] + raise AttributeError("Cannot change intents after the connection is established.") + self._connection._intents.value = value.value # noqa: SLF001 # pyright: ignore [reportPrivateUsage] + + +if not TYPE_CHECKING: + Context: ApplicationContext = ApplicationContext + +if TYPE_CHECKING: # temp fix for https://github.com/Pycord-Development/pycord/pull/2611 + type Context = ExtContext | ApplicationContext + ... # for some reason, this makes pycharm happy + +__all__ = ["ApplicationContext", "Bot", "Context", "ExtContext"] diff --git a/src/database/__init__.py b/src/database/__init__.py new file mode 100644 index 0000000..12d97d2 --- /dev/null +++ b/src/database/__init__.py @@ -0,0 +1 @@ +# Copyright (c) NiceBots diff --git a/src/database/config/__init__.py b/src/database/config/__init__.py new file mode 100644 index 0000000..2728099 --- /dev/null +++ b/src/database/config/__init__.py @@ -0,0 +1,40 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from logging import getLogger + +import aerich +from tortoise import Tortoise + +from src.config import config + +logger = getLogger("bot").getChild("database") + +TORTOISE_ORM = { + "connections": {"default": config["db"]["url"]}, + "apps": { + "models": { + "models": ["src.database.models", "aerich.models"], + "default_connection": "default", + } + }, +} + + +async def init() -> None: + command = aerich.Command( + TORTOISE_ORM, + app="models", + location="./src/database/migrations/", + ) + await command.init() + migrated = await command.upgrade(run_in_transaction=True) # pyright: ignore[reportUnknownVariableType] + logger.success(f"Successfully migrated {migrated} migrations") # pyright: ignore [reportAttributeAccessIssue] + await Tortoise.init(config=TORTOISE_ORM) + + +async def shutdown() -> None: + await Tortoise.close_connections() + + +__all__ = ["init", "shutdown"] diff --git a/src/database/migrations/models/0_20241224165004_init.py b/src/database/migrations/models/0_20241224165004_init.py new file mode 100644 index 0000000..0bb447d --- /dev/null +++ b/src/database/migrations/models/0_20241224165004_init.py @@ -0,0 +1,27 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "guild" ( + "id" BIGSERIAL NOT NULL PRIMARY KEY +); +COMMENT ON TABLE "guild" IS 'User model.'; +CREATE TABLE IF NOT EXISTS "user" ( + "id" BIGSERIAL NOT NULL PRIMARY KEY +); +COMMENT ON TABLE "user" IS 'User model.'; +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(100) NOT NULL, + "content" JSONB NOT NULL +);""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py new file mode 100644 index 0000000..e30c1dd --- /dev/null +++ b/src/database/models/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from .guild import Guild +from .user import User + +__all__ = [ + "Guild", + "User", +] diff --git a/src/database/models/guild.py b/src/database/models/guild.py new file mode 100644 index 0000000..beab722 --- /dev/null +++ b/src/database/models/guild.py @@ -0,0 +1,24 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from tortoise import fields +from tortoise.models import Model + + +class Guild(Model): + """User model. + + Represents a user in the database. + + Attributes + ---------- + id (int): Discord user ID. + free_credits (int): Amount of free credits the user has. + premium_credits (int): Amount of premium credits the user has. + + """ + + id: fields.Field[int] = fields.BigIntField(pk=True) + + +__all__ = ["Guild"] diff --git a/src/database/models/user.py b/src/database/models/user.py new file mode 100644 index 0000000..92a5c2e --- /dev/null +++ b/src/database/models/user.py @@ -0,0 +1,24 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from tortoise import fields +from tortoise.models import Model + + +class User(Model): + """User model. + + Represents a user in the database. + + Attributes + ---------- + id (int): Discord user ID. + free_credits (int): Amount of free credits the user has. + premium_credits (int): Amount of premium credits the user has. + + """ + + id: fields.Field[int] = fields.BigIntField(pk=True) + + +__all__ = ["User"] diff --git a/src/database/utils/__init__.py b/src/database/utils/__init__.py new file mode 100644 index 0000000..8e05d9a --- /dev/null +++ b/src/database/utils/__init__.py @@ -0,0 +1 @@ +# Copyright (c) NiceBots all rights reserved diff --git a/src/database/utils/preload.py b/src/database/utils/preload.py new file mode 100644 index 0000000..e867c51 --- /dev/null +++ b/src/database/utils/preload.py @@ -0,0 +1,86 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from discord.ext import commands + +from src import custom +from src.database.models import Guild, User + + +async def _preload_user(ctx: custom.Context) -> bool: + """Preload the user object into the context object. + + Args: + ---- + ctx: The context object to preload the user object into. + + Returns: + ------- + bool: (True) always. + + """ + if isinstance(ctx, custom.ExtContext): + ctx.user_obj = await User.get_or_none(id=ctx.author.id) if ctx.author else None + else: + ctx.user_obj = await User.get_or_none(id=ctx.user.id) if ctx.user else None + return True + + +preload_user = commands.check(_preload_user) # pyright: ignore [reportArgumentType] + + +async def _preload_guild(ctx: custom.Context) -> bool: + """Preload the guild object into the context object. + + Args: + ---- + ctx: The context object to preload the guild object into. + + Returns: + ------- + bool: (True) always. + + """ + ctx.guild_obj = await Guild.get_or_none(id=ctx.guild.id) if ctx.guild else None + return True + + +preload_guild = commands.check(_preload_guild) # pyright: ignore [reportArgumentType] + + +async def _preload_or_create_user(ctx: custom.Context) -> bool: + """Preload or create the user object into the context object. If the user object does not exist, create it. + + Args: + ---- + ctx: The context object to preload or create the user object into. + + Returns: + ------- + bool: (True) always. + + """ + ctx.user_obj, _ = await User.get_or_create(id=ctx.author.id) if ctx.author else (None, None) + return True + + +preload_or_create_user = commands.check(_preload_or_create_user) + + +async def _preload_or_create_guild(ctx: custom.Context) -> bool: + """Preload or create the guild object into the context object. If the guild object does not exist, create it. + + Args: + ---- + ctx: The context object to preload or create the guild object into. + + Returns: + ------- + bool: (True) always. + + """ + ctx.guild_obj, _ = await Guild.get_or_create(id=ctx.guild.id) if ctx.guild else (None, None) + return True + + +preload_or_create_guild = commands.check(_preload_or_create_guild) # pyright: ignore [reportArgumentType] diff --git a/src/extensions/__init__.py b/src/extensions/__init__.py new file mode 100644 index 0000000..8e05d9a --- /dev/null +++ b/src/extensions/__init__.py @@ -0,0 +1 @@ +# Copyright (c) NiceBots all rights reserved diff --git a/src/extensions/add-dm/__init__.py b/src/extensions/add-dm/__init__.py index 9c44609..191ddd8 100644 --- a/src/extensions/add-dm/__init__.py +++ b/src/extensions/add-dm/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .main import setup, default, schema +from .main import default, schema, setup -__all__ = ["setup", "default", "schema"] +__all__ = ["default", "schema", "setup"] diff --git a/src/extensions/add-dm/main.py b/src/extensions/add-dm/main.py index 195e7f0..5a37ed5 100644 --- a/src/extensions/add-dm/main.py +++ b/src/extensions/add-dm/main.py @@ -2,11 +2,10 @@ # SPDX-License-Identifier: MIT import discord - from discord.ext import commands from schema import Schema -from src.log import logger +from src.log import logger default = { "enabled": True, @@ -17,23 +16,21 @@ { "enabled": bool, "message": str, - } + }, ) class AddDM(commands.Cog): - def __init__(self, bot: discord.Bot, config: dict): + def __init__(self, bot: discord.Bot, config: dict) -> None: self.bot = bot self.config = config @discord.Cog.listener("on_guild_join") - async def on_join(self, guild: discord.Guild): + async def on_join(self, guild: discord.Guild) -> None: if not guild.me.guild_permissions.view_audit_log: return - entry = await guild.audit_logs( - limit=1, action=discord.AuditLogAction.bot_add - ).flatten() + entry = await guild.audit_logs(limit=1, action=discord.AuditLogAction.bot_add).flatten() user = entry[0].user try: await user.send(self.config["message"].format(user=user)) diff --git a/src/extensions/add-dm/readme.md b/src/extensions/add-dm/readme.md index 8130c25..41ded5a 100644 --- a/src/extensions/add-dm/readme.md +++ b/src/extensions/add-dm/readme.md @@ -1,30 +1,45 @@ # Add-DM Extension -The Add-DM extension is a valuable addition to your Botkit, designed to automatically send a direct message (DM) to the user who adds the bot to a guild. This feature enhances user engagement by providing immediate acknowledgment and guidance upon installation. +The Add-DM extension is a valuable addition to your Botkit, designed to automatically +send a direct message (DM) to the user who adds the bot to a guild. This feature +enhances user engagement by providing immediate acknowledgment and guidance upon +installation. ## Features -The Add-DM extension automatically triggers a DM to the user who adds the bot to a server. This message can be customized to include instructions, a welcome message, or any other information deemed necessary by the bot owner. It's an excellent way for bot developers to start engaging with new users right away. +The Add-DM extension automatically triggers a DM to the user who adds the bot to a +server. This message can be customized to include instructions, a welcome message, or +any other information deemed necessary by the bot owner. It's an excellent way for bot +developers to start engaging with new users right away. ## Usage -Upon the bot being added to a guild, it checks for the necessary permissions and identifies the user who added the bot. It then sends a predefined message to this user. The message can be customized in the bot's configuration file. +Upon the bot being added to a guild, it checks for the necessary permissions and +identifies the user who added the bot. It then sends a predefined message to this user. +The message can be customized in the bot's configuration file. ## Configuration -The Add-DM extension requires minimal configuration, allowing for the customization of the message sent to the user. Here's a basic outline of the configurable options: +The Add-DM extension requires minimal configuration, allowing for the customization of +the message sent to the user. Here's a basic outline of the configurable options: - `enabled`: Determines whether the Add-DM feature is active. Set to `True` by default. -- `message`: The message template sent to the user. Supports placeholders for dynamic content such as `{user.mention}` to mention the user. +- `message`: The message template sent to the user. Supports placeholders for dynamic + content such as `{user.mention}` to mention the user. -To customize the message, edit the `config.yml` file or set the appropriate environment variables. For example: +To customize the message, edit the `config.yml` file or set the appropriate environment +variables. For example: ```yaml add_dm: enabled: True - message: "Hello, {user.mention}! Thank you for adding me to your server. Type `/help` to see what I can do!" + message: + "Hello, {user.mention}! Thank you for adding me to your server. Type `/help` to see + what I can do!" ``` ## Contributing -Contributions to the Add-DM extension are welcome. If you have ideas on how to improve this extension or want to add new features, please submit a pull request. Your contributions are valuable in making this extension more useful for everyone. +Contributions to the Add-DM extension are welcome. If you have ideas on how to improve +this extension or want to add new features, please submit a pull request. Your +contributions are valuable in making this extension more useful for everyone. diff --git a/src/extensions/branding/__init__.py b/src/extensions/branding/__init__.py index 6694e6e..05b0b4b 100644 --- a/src/extensions/branding/__init__.py +++ b/src/extensions/branding/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .branding import setup, default, schema +from .branding import default, schema, setup -__all__ = ["setup", "default", "schema"] +__all__ = ["default", "schema", "setup"] diff --git a/src/extensions/branding/branding.py b/src/extensions/branding/branding.py index 3b19c53..686c4b4 100644 --- a/src/extensions/branding/branding.py +++ b/src/extensions/branding/branding.py @@ -2,14 +2,16 @@ # SPDX-License-Identifier: MIT import logging -import discord -import pytz import random from datetime import datetime +from typing import Any -from typing_extensions import TypedDict +import discord +import pytz from discord.ext import commands, tasks -from schema import Schema, And, Optional, Or +from schema import And, Optional, Or, Schema +from typing_extensions import TypedDict + from src.log import logger BASE_URL = "https://top.gg/api" @@ -41,7 +43,7 @@ Optional("listening"): Or(str, list[str]), Optional("streaming"): Or(str, list[str]), Optional("every"): And(int, lambda n: n > 0), - } + }, ) embed_config_schema = Schema( @@ -52,12 +54,12 @@ Optional("time"): bool, Optional("tz"): And(str, lambda s: s in pytz.all_timezones), Optional("separator"): str, - } + }, ), Optional("color"): Or(str, int), Optional("author_url"): str, Optional("author"): str, - } + }, ) schema = Schema( @@ -66,11 +68,9 @@ Optional("embed"): embed_config_schema, Optional("status"): And( status_schema, - lambda s: any( - k in ["playing", "watching", "listening", "streaming"] for k in s.keys() - ), + lambda s: any(k in ["playing", "watching", "listening", "streaming"] for k in s), ), - } + }, ) @@ -104,39 +104,36 @@ class Config(TypedDict): class Branding(commands.Cog): - def __init__(self, bot: discord.Bot, config: Config): + def __init__(self, bot: discord.Bot, config: Config) -> None: self.bot = bot self.config = config - if self.config.get("status"): - status: StatusConfig = self.config["status"] + if status := self.config.get("status"): if not status.get("every"): status["every"] = 60 * 5 - assert isinstance(status["every"], int), "status.every must be an integer" + if not isinstance(status["every"], int): + raise AssertionError("status.every must be an integer") - @tasks.loop(seconds=status["every"]) - async def update_status_loop(): - try: - await self.update_status() - except Exception as e: - logger.error(f"Error updating status: {e}") + @tasks.loop(seconds=status["every"], reconnect=True) + async def update_status_loop() -> None: + await self.update_status() self.update_status_loop = update_status_loop @commands.Cog.listener() - async def on_ready(self): + async def on_ready(self) -> None: if self.config.get("status"): self.update_status_loop.start() - def cog_unload(self): + def cog_unload(self) -> None: if self.config.get("status"): self.update_status_loop.cancel() - async def update_status(self): - status_types = list(self.config["status"].keys()) + async def update_status(self) -> None: + status_types = list(self.config["status"].keys()) # pyright: ignore [reportOptionalMemberAccess] status_types.remove("every") - status_type: str = random.choice(status_types) - status: str = random.choice(self.config["status"][status_type]) + status_type: str = random.choice(status_types) # noqa: S311 + status: str = random.choice(self.config["status"][status_type]) # noqa: S311 # pyright: ignore [reportOptionalSubscript, reportUnknownArgumentType] activity = discord.Activity( name=status, type=getattr(discord.ActivityType, status_type), @@ -144,11 +141,11 @@ async def update_status(self): await self.bot.change_presence(activity=activity) -def setup(bot: discord.Bot, config: dict): +def setup(bot: discord.Bot, config: dict[Any, Any]) -> None: if not config.get("embed") and not config.get("status"): logger.warning( "Branding extension is enabled but no configuration is provided for embed or status. You can disable this " - "extension or provide a configuration in the config.yaml file." + "extension or provide a configuration in the config.yaml file.", ) if config.get("embed"): embed: EmbedConfig = config["embed"] @@ -161,26 +158,23 @@ def setup(bot: discord.Bot, config: dict): footer["value"]: list[str] = [] if not footer.get("separator"): footer["separator"] = "|" - if color := embed.get("color"): - if isinstance(color, str): - embed["color"]: str = color.lstrip("#") - embed["color"]: int = int(embed["color"], 16) + if (color := embed.get("color")) and isinstance(color, str): + embed["color"]: str = color.lstrip("#") + embed["color"]: int = int(embed["color"], 16) class Embed(discord.Embed): - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) if footer: value: list[str] = footer["value"].copy() if footer.get("time"): - time: str = datetime.now( - pytz.timezone(footer.get("tz", "UTC")) - ).strftime(f"%d %B %Y at %H:%M ({footer.get('tz', 'UTC')})") + time: str = datetime.now(pytz.timezone(footer.get("tz", "UTC"))).strftime( + f"%d %B %Y at %H:%M ({footer.get('tz', 'UTC')})", + ) value.append(time) self.set_footer(text=f" {footer['separator']} ".join(value)) if embed.get("author"): - self.set_author( - name=embed["author"], icon_url=embed.get("author_url") - ) + self.set_author(name=embed["author"], icon_url=embed.get("author_url")) if embed.get("color") and not kwargs.get("color"): self.color = discord.Color(embed["color"]) diff --git a/src/extensions/branding/readme.md b/src/extensions/branding/readme.md index 51edb8d..118077f 100644 --- a/src/extensions/branding/readme.md +++ b/src/extensions/branding/readme.md @@ -1,24 +1,33 @@ # Branding Extension -The Branding extension is a versatile tool that allows you to customize your bot's presence and embeds. It is **enabled** by default. +The Branding extension is a versatile tool that allows you to customize your bot's +presence and embeds. It is **enabled** by default. ## Features The Branding extension performs the following tasks: -- It updates the bot's status every 5 minutes by default. The status can be set to playing, watching, listening, or streaming. +- It updates the bot's status every 5 minutes by default. The status can be set to + playing, watching, listening, or streaming. - It allows customization of the embed's footer, color, and author. ## Usage -The Branding extension is a background task and does not provide any commands for interaction. Once properly configured, it will automatically perform its tasks without any further intervention. +The Branding extension is a background task and does not provide any commands for +interaction. Once properly configured, it will automatically perform its tasks without +any further intervention. ## Configuration The Branding extension requires the following configuration: -- `status`: A dictionary that defines the bot's status. It can contain keys for playing, watching, listening, and streaming, each with a list of possible statuses. It also contains an `every` key that defines the interval (in seconds) at which the status is updated. -- `embed`: A dictionary that defines the embed's footer, color, and author. The footer can contain a value (a string or a list of strings), a boolean for whether to include the time, a timezone, and a separator. +- `status`: A dictionary that defines the bot's status. It can contain keys for playing, + watching, listening, and streaming, each with a list of possible statuses. It also + contains an `every` key that defines the interval (in seconds) at which the status is + updated. +- `embed`: A dictionary that defines the embed's footer, color, and author. The footer + can contain a value (a string or a list of strings), a boolean for whether to include + the time, a timezone, and a separator. Here is an example of how to configure the Branding extension in your `config.yml` file: @@ -27,7 +36,7 @@ extensions: branding: enabled: true status: - watching: ["you", "/help"] # in conjunction, you can also use streaming, playing, and listening + watching: ["you", "/help"] # in conjunction, you can also use streaming, playing, and listening every: 300 embed: footer: @@ -42,8 +51,10 @@ extensions: ## Important -Please note that the Branding extension will not load if both the `status` or `embed` configurations are not set up. The extension will log an error message and return. +Please note that the Branding extension will not load if both the `status` or `embed` +configurations are not set up. The extension will log an error message and return. ## Contributing -If you wish to contribute to the development of the Branding extension, please feel free to submit a pull request. We appreciate your help in making this extension better. +If you wish to contribute to the development of the Branding extension, please feel free +to submit a pull request. We appreciate your help in making this extension better. diff --git a/src/extensions/help/__init__.py b/src/extensions/help/__init__.py new file mode 100644 index 0000000..d5cca4f --- /dev/null +++ b/src/extensions/help/__init__.py @@ -0,0 +1,202 @@ +# Copyright (c) NiceBots all rights reserved +from collections import defaultdict +from functools import cached_property +from typing import Any, Final, final, override + +import discord +from discord.ext import commands +from discord.ext import pages as paginator + +from src import custom +from src.extensions.help.pages.classes import ( + HelpCategoryTranslation, +) +from src.i18n.classes import RawTranslation, TranslationWrapper, apply_locale + +from .pages import help_translation + + +def get_gradient_color(shade_index: int, color_index: int, max_shade: int = 50, max_color: int = 10) -> int: + """Generate a color from a two-dimensional gradient system using bright pastel colors. + + Args: + ---- + shade_index (int): Index for shade selection (0 to max_shade) + color_index (int): Index for base color selection (0 to max_color) + max_shade (int): Maximum value for shade_index (default 50) + max_color (int): Maximum value for color_index (default 10) + + Returns: + ------- + int: Color as a 24-bit integer + + """ + # Normalize indices to 0-1 range + shade_factor = max(0, min(1, shade_index / max_shade)) + color_factor = max(0, min(1, color_index / max_color)) + + # Bright pastel base colors + base_colors = [ + (179, 229, 252), # Bright light blue + (225, 190, 231), # Bright lilac + (255, 209, 220), # Bright pink + (255, 224, 178), # Bright peach + (255, 255, 198), # Bright yellow + (200, 230, 201), # Bright mint + (178, 255, 255), # Bright turquoise + (187, 222, 251), # Bright baby blue + (225, 190, 231), # Bright lavender + (255, 236, 179), # Bright cream + (200, 230, 255), # Bright sky blue + ] + + # Interpolate between colors based on color_factor + color_index_float = color_factor * (len(base_colors) - 1) + color_index_low = int(color_index_float) + color_index_high = min(color_index_low + 1, len(base_colors) - 1) + color_blend = color_index_float - color_index_low + + c1 = base_colors[color_index_low] + c2 = base_colors[color_index_high] + + # Interpolate between the two closest base colors + base_color = tuple(int(c1[i] * (1 - color_blend) + c2[i] * color_blend) for i in range(3)) + + # Modified shading approach for brighter colors + if shade_factor < 0.5: + # Darker shades: interpolate towards a very light gray instead of black + shade_factor_adjusted = shade_factor * 2 + # Increase minimum brightness (was 120, now 180) + darker_tone = tuple(max(c * 0.8, 180) for c in base_color) + final_color = tuple( + int(darker_tone[i] + (base_color[i] - darker_tone[i]) * shade_factor_adjusted) for i in range(3) + ) + else: + # Lighter shades: interpolate towards white + shade_factor_adjusted = (shade_factor - 0.5) * 2 + final_color = tuple( + int(base_color[i] * (1 - shade_factor_adjusted) + 255 * shade_factor_adjusted) for i in range(3) + ) + + # Convert to 24-bit integer + return (final_color[0] << 16) | (final_color[1] << 8) | final_color[2] + + +class PageIndicatorButton(paginator.PaginatorButton): + def __init__(self) -> None: + super().__init__(button_type="page_indicator", disabled=True, label="", style=discord.ButtonStyle.gray) + + +@final +class HelpView(paginator.Paginator): + def __init__( + self, + embeds: dict[str, list[discord.Embed]], + ui_translations: TranslationWrapper[dict[str, RawTranslation]], + bot: custom.Bot, + ) -> None: + self.bot = bot + + the_pages: list[paginator.PageGroup] = [ + paginator.PageGroup( + [paginator.Page(embeds=[embed]) for embed in data[1]], + label=data[0], + default=i == 0, + ) + for i, data in enumerate(embeds.items()) + ] + + self.embeds = embeds + self.ui_translations = ui_translations + self.page_indicator = PageIndicatorButton() + super().__init__( + the_pages, + show_menu=True, + menu_placeholder=ui_translations.select_category, + custom_buttons=[ + paginator.PaginatorButton("first", emoji="⏮️", style=discord.ButtonStyle.blurple), + paginator.PaginatorButton("prev", emoji="◀️", style=discord.ButtonStyle.red), + self.page_indicator, + paginator.PaginatorButton("next", emoji="▶️", style=discord.ButtonStyle.green), + paginator.PaginatorButton("last", emoji="⏭️", style=discord.ButtonStyle.blurple), + ], + use_default_buttons=False, + ) + + @override + def update_buttons(self) -> dict: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType] + r = super().update_buttons() # pyright: ignore [reportUnknownVariableType] + if self.show_indicator: + self.buttons["page_indicator"]["object"].label = self.ui_translations.page_indicator.format( + current=self.current_page + 1, total=self.page_count + 1 + ) + return r # pyright: ignore [reportUnknownVariableType] + + +def get_categories_embeds( + ui_translations: TranslationWrapper[dict[str, RawTranslation]], + categories: dict[str, TranslationWrapper[HelpCategoryTranslation]], + bot: custom.Bot, +) -> dict[str, list[discord.Embed]]: + embeds: defaultdict[str, list[discord.Embed]] = defaultdict(list) + for i, category in enumerate(categories): + for j, page in enumerate(category.pages.values()): # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue] + embed = discord.Embed( + title=f"{category.name} - {page.title}", # pyright: ignore [reportAttributeAccessIssue] + description=page.description, # pyright: ignore [reportUnknownArgumentType] + color=discord.Color(get_gradient_color(i, j)), + ) + if page.quick_tips: + embed.add_field(name=ui_translations.quick_tips_title, value="- " + "\n- ".join(page.quick_tips)) # pyright: ignore [reportUnknownArgumentType] + if page.examples: + embed.add_field(name=ui_translations.examples_title, value="- " + "\n- ".join(page.examples)) # pyright: ignore [reportUnknownArgumentType] + if page.related_commands: + embed.add_field( + name=ui_translations.related_commands_title, + value="- " + + "\n- ".join(bot.get_application_command(name).mention for name in page.related_commands), # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue, reportOptionalMemberAccess] + ) + embeds[category.name].append(embed) # pyright: ignore [reportAttributeAccessIssue] + return dict(embeds) + + +@final +class Help(commands.Cog): + def __init__(self, bot: custom.Bot, ui_translations: dict[str, RawTranslation], locales: set[str]) -> None: + self.bot = bot + self.ui_translations = ui_translations + self.locales = locales + + @cached_property + async def embeds(self) -> dict[str, dict[str, list[discord.Embed]]]: + embeds: defaultdict[str, dict[str, list[discord.Embed]]] = defaultdict(dict) + for locale in self.locales: + t = help_translation.get_for_locale(locale) + ui = apply_locale(self.ui_translations, locale) + embeds[locale] = get_categories_embeds(ui, t.categories, self.bot) + return dict(embeds) + + @discord.slash_command( + name="help", + integration_types={discord.IntegrationType.user_install, discord.IntegrationType.guild_install}, + contexts={ + discord.InteractionContextType.guild, + discord.InteractionContextType.private_channel, + discord.InteractionContextType.bot_dm, + }, + ) + async def help_slash(self, ctx: custom.ApplicationContext) -> None: + paginator = HelpView( + embeds=self.embeds[ctx.locale], + ui_translations=apply_locale(self.ui_translations, ctx.locale), + bot=self.bot, + ) + await paginator.respond(ctx.interaction, ephemeral=True) + + +def setup(bot: custom.Bot, config: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny] + bot.add_cog(Help(bot, config["translations"], set(config["locales"]))) + + +default: Final = {"enabled": False} +__all__ = ["default", "setup"] diff --git a/src/extensions/help/pages/__init__.py b/src/extensions/help/pages/__init__.py new file mode 100644 index 0000000..01ff1c8 --- /dev/null +++ b/src/extensions/help/pages/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from itertools import chain +from pathlib import Path + +import yaml + +from .classes import HelpCategoryTranslation, HelpTranslation + +# iterate over .y[a]ml files in the same directory as this file +categories: list[HelpCategoryTranslation] = [] + +for file in chain(Path(__file__).parent.glob("*.yaml"), Path(__file__).parent.glob("*.yml")): + with open(file, encoding="utf-8") as f: + data = yaml.safe_load(f) + categories.append(HelpCategoryTranslation(**data)) + +categories.sort(key=lambda item: item.order) + +help_translation = HelpTranslation(categories=categories) diff --git a/src/extensions/help/pages/classes.py b/src/extensions/help/pages/classes.py new file mode 100644 index 0000000..49b063c --- /dev/null +++ b/src/extensions/help/pages/classes.py @@ -0,0 +1,24 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from src.i18n.classes import RawTranslation, Translation + + +class HelpPageTranslation(Translation): + title: RawTranslation + description: RawTranslation + category: RawTranslation + quick_tips: list[RawTranslation] | None = None + examples: list[RawTranslation] | None = None + related_commands: list[str] | None = None + + +class HelpCategoryTranslation(Translation): + name: RawTranslation + description: RawTranslation + pages: dict[str, HelpPageTranslation] + order: int # For sorting categories in the dropdown + + +class HelpTranslation(Translation): + categories: list[HelpCategoryTranslation] diff --git a/src/extensions/help/translations.yml b/src/extensions/help/translations.yml new file mode 100644 index 0000000..62c5864 --- /dev/null +++ b/src/extensions/help/translations.yml @@ -0,0 +1,25 @@ +# Copyright (c) NiceBots all rights reserved +strings: + quick_tips_title: + en-US: Tips & Tricks + es-ES: Consejos y Trucos + examples_title: + en-US: Usage Examples + es-ES: Ejemplos de Uso + related_commands_title: + en-US: Available Commands + es-ES: Comandos Disponibles + select_category: + en-US: Select a category + es-ES: Selecciona una categoría + page_indicator: + en-US: Page {current} of {total} + es-ES: Página {current} de {total} +commands: + help: + name: + en-US: help + es-ES: ayuda + description: + en-US: Get help with using the bot + es-ES: Obtén ayuda para usar el bot diff --git a/src/extensions/listings/__init__.py b/src/extensions/listings/__init__.py index 9c44609..191ddd8 100644 --- a/src/extensions/listings/__init__.py +++ b/src/extensions/listings/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .main import setup, default, schema +from .main import default, schema, setup -__all__ = ["setup", "default", "schema"] +__all__ = ["default", "schema", "setup"] diff --git a/src/extensions/listings/main.py b/src/extensions/listings/main.py index 727fd45..875ce76 100644 --- a/src/extensions/listings/main.py +++ b/src/extensions/listings/main.py @@ -1,11 +1,13 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import discord -import aiohttp +from typing import Any, override +import aiohttp +import discord from discord.ext import commands, tasks -from schema import Schema, Optional +from schema import Optional, Schema + from src.log import logger TOPGG_BASE_URL = "https://top.gg/api" @@ -20,18 +22,17 @@ Optional("topgg_token"): str, Optional("discordscom_token"): str, "enabled": bool, - } + }, ) -async def post_request(url: str, headers: dict, payload: dict): - async with aiohttp.ClientSession() as session: - async with session.post(url, headers=headers, json=payload) as resp: - # raise the eventual status code - resp.raise_for_status() +async def post_request(url: str, headers: dict[Any, Any], payload: dict[Any, Any]) -> None: + async with aiohttp.ClientSession() as session, session.post(url, headers=headers, json=payload) as resp: + # raise the eventual status code + resp.raise_for_status() -async def try_post_request(url: str, headers: dict, payload: dict): +async def try_post_request(url: str, headers: dict[Any, Any], payload: dict[Any, Any]) -> None: try: await post_request(url, headers, payload) except aiohttp.ClientResponseError as e: @@ -39,53 +40,58 @@ async def try_post_request(url: str, headers: dict, payload: dict): logger.error("Invalid token") else: logger.error(e) - except Exception as e: - logger.error(e) + except Exception: # noqa: BLE001 + logger.exception(f"Failed to post request to {url}") class Listings(commands.Cog): - def __init__(self, bot: discord.Bot, config: dict): + def __init__(self, bot: discord.Bot, config: dict[Any, Any]) -> None: self.bot: discord.Bot = bot - self.config: dict = config + self.config: dict[Any, Any] = config self.topgg = bool(config.get("topgg_token")) self.discordscom = bool(config.get("discordscom_token")) @commands.Cog.listener("on_ready") - async def on_ready(self): + async def on_ready(self) -> None: self.update_count_loop.start() + @override def cog_unload(self) -> None: self.update_count_loop.cancel() @tasks.loop(minutes=30) - async def update_count_loop(self): + async def update_count_loop(self) -> None: try: if self.topgg: await self.update_count_topgg() if self.discordscom: await self.update_count_discordscom() - except Exception as e: - print(e) + except Exception: # noqa: BLE001 + logger.exception("Failed to update count") - async def update_count_discordscom(self): - headers = { + async def update_count_discordscom(self) -> None: + headers: dict[str, str] = { "Authorization": self.config["discordscom_token"], "Content-Type": "application/json", } payload = {"server_count": len(self.bot.guilds)} + if not self.bot.user: + return url = f"{DISCORDSCOM_BASE_URL}/{self.bot.user.id}/setservers" await try_post_request(url, headers, payload) logger.info("Updated discords.com count") - async def update_count_topgg(self): - headers = {"Authorization": self.config["topgg_token"]} + async def update_count_topgg(self) -> None: + headers: dict[str, str] = {"Authorization": self.config["topgg_token"]} payload = {"server_count": len(self.bot.guilds)} + if not self.bot.user: + return url = f"{TOPGG_BASE_URL}/bots/{self.bot.user.id}/stats" await try_post_request(url, headers, payload) logger.info("Updated top.gg count") -def setup(bot: discord.Bot, config: dict): +def setup(bot: discord.Bot, config: dict[Any, Any]) -> None: if not config.get("topgg_token") and not config.get("discordscom_token"): logger.error("Top.gg or Discords.com token not found") return diff --git a/src/extensions/listings/readme.md b/src/extensions/listings/readme.md index 0b38379..c336fea 100644 --- a/src/extensions/listings/readme.md +++ b/src/extensions/listings/readme.md @@ -1,6 +1,8 @@ # Listings Extension -The Listings extension is a powerful tool that allows your bot to interact with various bot listing websites. It requires valid tokens for each listing website and is **disabled** by default. +The Listings extension is a powerful tool that allows your bot to interact with various +bot listing websites. It requires valid tokens for each listing website and is +**disabled** by default. ## Features @@ -11,15 +13,20 @@ The Listings extension performs the following tasks: ## Usage -The Listings extension is a background task and does not provide any commands for interaction. Once enabled and properly configured, it will automatically perform its tasks without any further intervention. +The Listings extension is a background task and does not provide any commands for +interaction. Once enabled and properly configured, it will automatically perform its +tasks without any further intervention. ## Configuration The Listings extension requires the following configuration: -- `topgg_token`: Your Top.gg token. This is a string, and it is required for the Top.gg listing to work. -- `discordscom_token`: Your Discords.com token. This is a string, and it is required for the Discords.com listing to work. -- `enabled`: A boolean value that determines whether the extension is enabled or not. By default, this is set to `false`. +- `topgg_token`: Your Top.gg token. This is a string, and it is required for the Top.gg + listing to work. +- `discordscom_token`: Your Discords.com token. This is a string, and it is required for + the Discords.com listing to work. +- `enabled`: A boolean value that determines whether the extension is enabled or not. By + default, this is set to `false`. Here is an example of how to configure the Listings extension in your `config.yml` file: @@ -31,12 +38,15 @@ extensions: enabled: true ``` -Please replace `"your-topgg-token"` and `"your-discordscom-token"` with your actual tokens. +Please replace `"your-topgg-token"` and `"your-discordscom-token"` with your actual +tokens. ## Important -Please note that the Listings extension will not load if the `topgg_token` or `discordscom_token` is not set up. The extension will log an error message and return. +Please note that the Listings extension will not load if the `topgg_token` or +`discordscom_token` is not set up. The extension will log an error message and return. ## Contributing -If you wish to contribute to the development of the Listings extension, please feel free to submit a pull request. We appreciate your help in making this extension better. \ No newline at end of file +If you wish to contribute to the development of the Listings extension, please feel free +to submit a pull request. We appreciate your help in making this extension better. diff --git a/src/extensions/nice-errors/__init__.py b/src/extensions/nice-errors/__init__.py deleted file mode 100644 index 9c44609..0000000 --- a/src/extensions/nice-errors/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) NiceBots.xyz -# SPDX-License-Identifier: MIT - -from .main import setup, default, schema - -__all__ = ["setup", "default", "schema"] diff --git a/src/extensions/nice-errors/handler.py b/src/extensions/nice-errors/handler.py deleted file mode 100644 index c1a8098..0000000 --- a/src/extensions/nice-errors/handler.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) NiceBots.xyz -# SPDX-License-Identifier: MIT - -from typing import Any -from typing_extensions import override -import discord -from discord.interactions import Interaction -from src.i18n.classes import RawTranslation -from src.i18n import apply_locale -from src import custom -import difflib -from discord.ext import commands, bridge - -sentry_sdk = None - -try: - import sentry_sdk -except ImportError: - pass - - -class RunInsteadButton(discord.ui.Button[discord.ui.View]): - def __init__( - self, - label: str, - *, - ctx: custom.ExtContext, - instead: bridge.BridgeExtCommand | commands.Command[Any, Any, Any], - ): - self.ctx = ctx - self.instead = instead - super().__init__(style=discord.ButtonStyle.green, label=label) - - @override - async def callback(self, interaction: Interaction): - if ( - not interaction.user - or not self.ctx.author # pyright: ignore[reportUnnecessaryComparison] - or not interaction.user.id == self.ctx.author.id # pyright: ignore[reportFunctionMemberAccess] - ): - await interaction.respond(":x: Nope", ephemeral=True) - return - await self.instead.invoke(self.ctx) - await interaction.response.defer() - if interaction.message: - try: - await interaction.message.delete() - except discord.HTTPException: - pass - - -def find_most_similar(word: str, word_list: list[str]) -> str | None: - if result := difflib.get_close_matches(word, word_list, n=1, cutoff=0.6): - return result[0] - return None - - -def find_similar_command( - ctx: custom.ExtContext, -) -> bridge.BridgeExtCommand | commands.Command[Any, Any, Any] | None: - command: str | None = ctx.invoked_with - if not command: - return None - if not isinstance(ctx.bot, custom.Bot): - return None - command_list: dict[ - str, bridge.BridgeExtCommand | commands.Command[Any, Any, Any] - ] = {cmd.name: cmd for cmd in ctx.bot.commands} # pyright: ignore[reportUnknownVariableType] - similar_command: str | None = find_most_similar(command, list(command_list.keys())) - if similar_command: - return command_list.get(similar_command) - return None - - -def get_locale(ctx: custom.Context | Interaction) -> str | None: - locale: str | None = None - if isinstance(ctx, custom.ApplicationContext): - locale = ctx.locale or ctx.guild_locale - elif isinstance(ctx, custom.ExtContext): - if ctx.guild: # pyright: ignore[reportUnnecessaryComparison] # for some reason pyright thinks guild is function - locale = ctx.guild.preferred_locale # pyright: ignore[reportFunctionMemberAccess] - elif isinstance(ctx, Interaction): # pyright: ignore[reportUnnecessaryIsInstance] # we want to really make sure - locale = ctx.locale or ctx.guild_locale - return locale - - -async def handle_error( - error: Exception | discord.ApplicationCommandInvokeError, - ctx: discord.Interaction | custom.Context, - /, - raw_translations: dict[str, RawTranslation], - use_sentry_sdk: bool = False, -): - original_error = error - report: bool = True - sendargs: dict[str, Any] = {} - translations = apply_locale(raw_translations, get_locale(ctx)) - if isinstance(error, discord.ApplicationCommandInvokeError): - original_error = error.original - if isinstance(error, commands.CommandNotFound) and isinstance( - ctx, custom.ExtContext - ): - if similar_command := find_similar_command(ctx): - message = translations.error_command_not_found.format( - similar_command=similar_command.name - ) - view = discord.ui.View( - RunInsteadButton( - translations.run_x_instead.format(command=similar_command.name), - ctx=ctx, - instead=similar_command, - ), - disable_on_timeout=True, - timeout=60, # 1 minute - ) - sendargs["view"] = view - report = False # this is not an error in the program - else: - return # this is not an error in the program - elif isinstance(error, discord.Forbidden): - message = ( - translations.error_missing_permissions - + f"\n`{original_error.args[0].split(':')[-1].strip()}`" - ) - else: - message = translations.error_generic - if report and use_sentry_sdk and sentry_sdk: - out = sentry_sdk.capture_exception(error) - message += f"\n\n-# {translations.reported_to_devs} - `{out}`" - # capture the error *before* sending the message to avoid errors in the error handlers - await ctx.respond(message, ephemeral=True, **sendargs) - if report: - raise error diff --git a/src/extensions/nice-errors/readme.md b/src/extensions/nice-errors/readme.md deleted file mode 100644 index 48732d3..0000000 --- a/src/extensions/nice-errors/readme.md +++ /dev/null @@ -1,28 +0,0 @@ -# Nice-Errors Extension - -The Nice-Errors extension is an essential tool for your Botkit, designed to enhance error handling by providing user-friendly error messages during command execution. This feature improves the user experience by ensuring that errors are communicated effectively and clearly. - -## Features - -The Nice-Errors extension intercepts errors that occur during the execution of application commands. Instead of displaying raw error messages, it formats them into more understandable text and provides guidance or feedback to the user. This can significantly improve the interaction between the bot and its users by making error messages less intimidating and more informative. - -## Usage - -When a command execution leads to an error, the Nice-Errors extension automatically catches this error. It then checks the type of error and responds with a customized, user-friendly message. This process is entirely automated, requiring no manual intervention from the user or developer. - -## Configuration - -The Nice-Errors extension can be enabled or disabled as needed. By default, it is enabled to ensure that your bot always provides helpful feedback to users during errors. Here's how you can configure it: - -- `enabled`: A boolean value that determines whether the Nice-Errors feature is active. Set to `True` by default. - -To adjust this setting, you can modify the `config.yml` file or use environment variables. For example: - -```yaml -nice_errors: - enabled: True -``` - -## Contributing - -Contributions to the Nice-Errors extension are highly encouraged. If you have suggestions for improving the error messages or adding support for more types of errors, please submit a pull request. Your input is invaluable in making this extension more effective for all users. diff --git a/src/extensions/nice_errors/__init__.py b/src/extensions/nice_errors/__init__.py new file mode 100644 index 0000000..191ddd8 --- /dev/null +++ b/src/extensions/nice_errors/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) NiceBots.xyz +# SPDX-License-Identifier: MIT + +from .main import default, schema, setup + +__all__ = ["default", "schema", "setup"] diff --git a/src/extensions/nice_errors/handlers/__init__.py b/src/extensions/nice_errors/handlers/__init__.py new file mode 100644 index 0000000..c9114c5 --- /dev/null +++ b/src/extensions/nice_errors/handlers/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from .base import ErrorHandlerManager + +error_handler = ErrorHandlerManager() diff --git a/src/extensions/nice_errors/handlers/base.py b/src/extensions/nice_errors/handlers/base.py new file mode 100644 index 0000000..2c8787b --- /dev/null +++ b/src/extensions/nice_errors/handlers/base.py @@ -0,0 +1,126 @@ +# Copyright (c) NiceBots.xyz +# SPDX-License-Identifier: MIT + +import contextlib +import logging +from abc import ABC, abstractmethod +from typing import Any, final, overload + +import discord +import sentry_sdk + +from src import custom +from src.i18n.classes import RawTranslation, apply_locale + +logger = logging.getLogger("bot").getChild("nice_errors").getChild("handlers") + + +def _get_locale(ctx: custom.Context | discord.Interaction) -> str | None: + locale: str | None = None + if isinstance(ctx, custom.ApplicationContext): + locale = ctx.locale or ctx.guild_locale + elif isinstance(ctx, custom.ExtContext): + if ctx.guild: + locale = ctx.guild.preferred_locale + elif isinstance(ctx, discord.Interaction): # pyright: ignore[reportUnnecessaryIsInstance] + locale = ctx.locale or ctx.guild_locale + return locale + + +class BaseErrorHandler[E: Exception](ABC): + def __init__(self, error_cls: type[E]) -> None: + self.error_cls: type[E] = error_cls + + @abstractmethod + async def __call__( + self, + error: E, + ctx: custom.Context | discord.Interaction, + sendargs: dict[str, Any], + message: str, + report: bool, + ) -> "ErrorHandlerRType": ... + + @staticmethod + def _get_locale(ctx: custom.Context | discord.Interaction) -> str | None: + return _get_locale(ctx) + + +type ErrorHandlerRType = tuple[bool, bool, str, dict[str, Any]] +type ErrorHandlerType[E: Exception] = BaseErrorHandler[E] +type ErrorHandlersType[E: Exception] = dict[ + type[E] | None, + ErrorHandlerType[E], +] + + +@final +class ErrorHandlerManager: + def __init__(self, error_handlers: ErrorHandlersType[Exception] | None = None) -> None: + self.error_handlers: ErrorHandlersType[Exception] = error_handlers or {} + + def _get_handler(self, error: Exception) -> BaseErrorHandler[Exception] | None: + if handler := self.error_handlers.get(type(error)): + return handler + with contextlib.suppress(StopIteration): + return next( + filter(lambda x: x[0] is not None and issubclass(type(error), x[0]), self.error_handlers.items()) + )[1] + return None + + async def handle_error( + self, + error: Exception | discord.ApplicationCommandInvokeError, + ctx: discord.Interaction | custom.Context, + /, + raw_translations: dict[str, RawTranslation], + use_sentry_sdk: bool = False, + ) -> None: + original_error = error + report: bool = True + sendargs: dict[str, Any] = { + "ephemeral": True, + } + message: str = "" + translations = apply_locale(raw_translations, _get_locale(ctx)) + if isinstance(error, discord.ApplicationCommandInvokeError): + original_error = error.original + + handler = self._get_handler(original_error) or self.error_handlers.get(None) + if handler: + handled, report, message, sendargs = await handler( + original_error, + ctx, + sendargs, + str(error), + report, + ) + if handled: + return + if report and use_sentry_sdk: + out = sentry_sdk.capture_exception(error) + message += f"\n\n-# {translations.reported_to_devs} - `{out}`" + await ctx.respond(message, **sendargs) + if report: + raise error + + @overload + def add_error_handler[E: Exception](self, error: None, handler: ErrorHandlerType[Exception]) -> None: ... + + @overload + def add_error_handler[E: Exception](self, error: type[E], handler: ErrorHandlerType[E]) -> None: ... + + def add_error_handler[E: Exception](self, error: type[E] | None, handler: ErrorHandlerType[E | Exception]) -> None: + logger.info( + f"Adding error handler {handler.__class__.__qualname__} for {error.__qualname__ if error is not None else 'Generic'}" # noqa: E501 + ) + self.error_handlers[error] = handler + + +__all__ = ( + "BaseErrorHandler", + "ErrorHandlerManager", + "ErrorHandlerRType", + "ErrorHandlerType", + "ErrorHandlersType", +) diff --git a/src/extensions/nice_errors/handlers/cooldown.py b/src/extensions/nice_errors/handlers/cooldown.py new file mode 100644 index 0000000..d1b4fe7 --- /dev/null +++ b/src/extensions/nice_errors/handlers/cooldown.py @@ -0,0 +1,36 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from typing import Any, final, override + +import discord + +from src import custom +from src.i18n.classes import RawTranslation, apply_locale +from src.utils.cooldown import CooldownExceeded + +from .base import BaseErrorHandler, ErrorHandlerRType + + +@final +class CooldownErrorHandler(BaseErrorHandler[CooldownExceeded]): + def __init__(self, translations: dict[str, RawTranslation]) -> None: + self.translations = translations + super().__init__(CooldownExceeded) + + @override + async def __call__( + self, + error: CooldownExceeded, + ctx: custom.Context | discord.Interaction, + sendargs: dict[str, Any], + message: str, + report: bool, + ) -> ErrorHandlerRType: + translations = apply_locale(self.translations, self._get_locale(ctx)) + + message = translations.error_cooldown_exceeded + + sendargs["ephemeral"] = True + + return False, False, message, sendargs diff --git a/src/extensions/nice_errors/handlers/forbidden.py b/src/extensions/nice_errors/handlers/forbidden.py new file mode 100644 index 0000000..d041d57 --- /dev/null +++ b/src/extensions/nice_errors/handlers/forbidden.py @@ -0,0 +1,33 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from typing import Any, final, override + +import discord + +from src import custom +from src.i18n.classes import RawTranslation, apply_locale + +from .base import BaseErrorHandler, ErrorHandlerRType + + +@final +class ForbiddenErrorHandler(BaseErrorHandler[discord.Forbidden]): + def __init__(self, translations: dict[str, RawTranslation]) -> None: + self.translations = translations + super().__init__(discord.Forbidden) + + @override + async def __call__( + self, + error: discord.Forbidden, + ctx: custom.Context | discord.Interaction, + sendargs: dict[str, Any], + message: str, + report: bool, + ) -> ErrorHandlerRType: + translations = apply_locale(self.translations, self._get_locale(ctx)) + + message = translations.error_missing_permissions + f"\n`{error.args[0].split(':')[-1].strip()}`" + + return False, report, message, sendargs diff --git a/src/extensions/nice_errors/handlers/generic.py b/src/extensions/nice_errors/handlers/generic.py new file mode 100644 index 0000000..79636c3 --- /dev/null +++ b/src/extensions/nice_errors/handlers/generic.py @@ -0,0 +1,33 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +from typing import Any, final, override + +import discord + +from src import custom +from src.i18n.classes import RawTranslation, apply_locale + +from .base import BaseErrorHandler, ErrorHandlerRType + + +@final +class GenericErrorHandler(BaseErrorHandler[Exception]): + def __init__(self, translations: dict[str, RawTranslation]) -> None: + self.translations = translations + super().__init__(Exception) + + @override + async def __call__( + self, + error: Exception, + ctx: custom.Context | discord.Interaction, + sendargs: dict[str, Any], + message: str, + report: bool, + ) -> ErrorHandlerRType: + translations = apply_locale(self.translations, self._get_locale(ctx)) + + message = translations.error_generic + + return False, report, message, sendargs diff --git a/src/extensions/nice_errors/handlers/not_found.py b/src/extensions/nice_errors/handlers/not_found.py new file mode 100644 index 0000000..e021455 --- /dev/null +++ b/src/extensions/nice_errors/handlers/not_found.py @@ -0,0 +1,94 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +import contextlib +import difflib +from typing import Any, final, override + +import discord +from discord.ext import bridge, commands + +from src import custom +from src.i18n.classes import RawTranslation, apply_locale + +from .base import BaseErrorHandler, ErrorHandlerRType + + +def find_most_similar(word: str, word_list: list[str]) -> str | None: + if result := difflib.get_close_matches(word, word_list, n=1, cutoff=0.6): + return result[0] + return None + + +def find_similar_command( + ctx: custom.ExtContext, +) -> bridge.BridgeExtCommand | commands.Command[Any, Any, Any] | None: + command: str | None = ctx.invoked_with + if not command: + return None + command_list: dict[str, bridge.BridgeExtCommand | commands.Command[Any, Any, Any]] = { + cmd.name: cmd + for cmd in ctx.bot.commands # pyright: ignore[reportUnknownVariableType] + } + similar_command: str | None = find_most_similar(command, list(command_list.keys())) + if similar_command: + return command_list.get(similar_command) + return None + + +class RunInsteadButton(discord.ui.Button[discord.ui.View]): + def __init__( + self, + label: str, + *, + ctx: custom.ExtContext, + instead: bridge.BridgeExtCommand | commands.Command[Any, Any, Any], + ) -> None: + self.ctx: custom.ExtContext = ctx + self.instead: bridge.BridgeExtCommand | commands.Command[Any, Any, Any] = instead + super().__init__(style=discord.ButtonStyle.green, label=label) + + @override + async def callback(self, interaction: discord.Interaction) -> None: + if not interaction.user or not self.ctx.author or interaction.user.id != self.ctx.author.id: + await interaction.respond(":x: Nope", ephemeral=True) + return + await self.instead.invoke(self.ctx) + await interaction.response.defer() + if interaction.message: + with contextlib.suppress(discord.HTTPException): + await interaction.message.delete() + + +@final +class NotFoundErrorHandler(BaseErrorHandler[commands.CommandNotFound]): + def __init__(self, translations: dict[str, RawTranslation]) -> None: + self.translations = translations + super().__init__(commands.CommandNotFound) + + @override + async def __call__( + self, + error: commands.CommandNotFound, + ctx: custom.Context | discord.Interaction, + sendargs: dict[str, Any], + message: str, + report: bool, + ) -> ErrorHandlerRType: + if not isinstance(ctx, custom.ExtContext): + return False, report, message, sendargs + translations = apply_locale(self.translations, self._get_locale(ctx)) + if similar_command := find_similar_command(ctx): + message = translations.error_command_not_found.format(similar_command=similar_command.name) + view = discord.ui.View( + RunInsteadButton( + translations.run_x_instead.format(command=similar_command.name), + ctx=ctx, + instead=similar_command, + ), + disable_on_timeout=True, + timeout=60, # 1 minute + ) + sendargs["view"] = view + return False, False, message, sendargs + return True, False, message, sendargs diff --git a/src/extensions/nice-errors/main.py b/src/extensions/nice_errors/main.py similarity index 50% rename from src/extensions/nice-errors/main.py rename to src/extensions/nice_errors/main.py index 1bc0c71..67ed994 100644 --- a/src/extensions/nice-errors/main.py +++ b/src/extensions/nice_errors/main.py @@ -1,14 +1,20 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import discord +from typing import Any, final +import discord from discord.ext import commands -from schema import Schema, Optional +from schema import Optional, Schema from src import custom -from .handler import handle_error -from typing import Any +from src.utils.cooldown import CooldownExceeded + +from .handlers import error_handler +from .handlers.cooldown import CooldownErrorHandler +from .handlers.forbidden import ForbiddenErrorHandler +from .handlers.generic import GenericErrorHandler +from .handlers.not_found import NotFoundErrorHandler default = { "enabled": True, @@ -18,12 +24,13 @@ { "enabled": bool, Optional("sentry"): {"dsn": str}, - } + }, ) +@final class NiceErrors(commands.Cog): - def __init__(self, bot: discord.Bot, sentry_sdk: bool, config: dict[str, Any]): + def __init__(self, bot: discord.Bot, sentry_sdk: bool, config: dict[str, Any]) -> None: self.bot = bot self.sentry_sdk = sentry_sdk self.config = config @@ -33,8 +40,8 @@ async def on_error( self, ctx: custom.ApplicationContext, error: discord.ApplicationCommandInvokeError, - ): - await handle_error( + ) -> None: + await error_handler.handle_error( error, ctx, raw_translations=self.config["translations"], @@ -42,16 +49,21 @@ async def on_error( ) @discord.Cog.listener("on_command_error") - async def on_command_error( - self, ctx: custom.ExtContext, error: commands.CommandError - ): - await handle_error( + async def on_command_error(self, ctx: custom.ExtContext, error: commands.CommandError) -> None: + await error_handler.handle_error( error, ctx, raw_translations=self.config["translations"], use_sentry_sdk=self.sentry_sdk, ) + def add_error_handler(self, *args: Any, **kwargs: Any) -> None: + error_handler.add_error_handler(*args, **kwargs) + def setup(bot: custom.Bot, config: dict[str, Any]) -> None: bot.add_cog(NiceErrors(bot, bool(config.get("sentry", {}).get("dsn")), config)) + error_handler.add_error_handler(None, GenericErrorHandler(config["translations"])) + error_handler.add_error_handler(commands.CommandNotFound, NotFoundErrorHandler(config["translations"])) + error_handler.add_error_handler(discord.Forbidden, ForbiddenErrorHandler(config["translations"])) + error_handler.add_error_handler(CooldownExceeded, CooldownErrorHandler(config["translations"])) diff --git a/src/extensions/nice-errors/patch.py b/src/extensions/nice_errors/patch.py similarity index 51% rename from src/extensions/nice-errors/patch.py rename to src/extensions/nice_errors/patch.py index 9c8c3b9..12bf81e 100644 --- a/src/extensions/nice-errors/patch.py +++ b/src/extensions/nice_errors/patch.py @@ -1,20 +1,25 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from typing import Any -from .handler import handle_error +from typing import TYPE_CHECKING, Any +from src.log import logger as base_logger -async def patch(config: dict[str, Any]): +from .handlers import error_handler + +logger = base_logger.getChild("nice_errors") + + +async def patch(config: dict[str, Any]) -> None: sentry_sdk = None if config.get("sentry", {}).get("dsn"): import sentry_sdk - from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.asyncio import AsyncioIntegration + from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.scrubber import ( - EventScrubber, DEFAULT_DENYLIST, DEFAULT_PII_DENYLIST, + EventScrubber, ) sentry_sdk.init( @@ -28,25 +33,25 @@ async def patch(config: dict[str, Any]): pii_denylist=[*DEFAULT_PII_DENYLIST, "headers", "kwargs"], ), ) + logger.success("Sentry SDK initialized") + + from discord.ui import View + + if TYPE_CHECKING: + from discord import Interaction + from discord.ui import Item + + async def on_error( + self: View, # noqa: ARG001 + error: Exception, + item: "Item[View]", # noqa: ARG001 + interaction: "Interaction", + ) -> None: + await error_handler.handle_error( + error, + interaction, + raw_translations=config["translations"], + use_sentry_sdk=bool(sentry_sdk), + ) - import discord - from discord import Interaction - from discord.ui import Item - from typing_extensions import override - - class PatchedView(discord.ui.View): - @override - async def on_error( - self, - error: Exception, - item: Item, # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] - interaction: Interaction, - ) -> None: - await handle_error( - error, - interaction, - raw_translations=config["translations"], - use_sentry_sdk=bool(sentry_sdk), - ) - - discord.ui.View = PatchedView + View.on_error = on_error diff --git a/src/extensions/nice_errors/readme.md b/src/extensions/nice_errors/readme.md new file mode 100644 index 0000000..8eea030 --- /dev/null +++ b/src/extensions/nice_errors/readme.md @@ -0,0 +1,45 @@ +# Nice-Errors Extension + +The Nice-Errors extension is an essential tool for your Botkit, designed to enhance +error handling by providing user-friendly error messages during command execution. This +feature improves the user experience by ensuring that errors are communicated +effectively and clearly. + +## Features + +The Nice-Errors extension intercepts errors that occur during the execution of +application commands. Instead of displaying raw error messages, it formats them into +more understandable text and provides guidance or feedback to the user. This can +significantly improve the interaction between the bot and its users by making error +messages less intimidating and more informative. + +## Usage + +When a command execution leads to an error, the Nice-Errors extension automatically +catches this error. It then checks the type of error and responds with a customized, +user-friendly message. This process is entirely automated, requiring no manual +intervention from the user or developer. + +## Configuration + +The Nice-Errors extension can be enabled or disabled as needed. By default, it is +enabled to ensure that your bot always provides helpful feedback to users during errors. +Here's how you can configure it: + +- `enabled`: A boolean value that determines whether the Nice-Errors feature is active. + Set to `True` by default. + +To adjust this setting, you can modify the `config.yml` file or use environment +variables. For example: + +```yaml +nice_errors: + enabled: True +``` + +## Contributing + +Contributions to the Nice-Errors extension are highly encouraged. If you have +suggestions for improving the error messages or adding support for more types of errors, +please submit a pull request. Your input is invaluable in making this extension more +effective for all users. diff --git a/src/extensions/nice-errors/translations.yml b/src/extensions/nice_errors/translations.yml similarity index 78% rename from src/extensions/nice-errors/translations.yml rename to src/extensions/nice_errors/translations.yml index 7c14820..5ac630a 100644 --- a/src/extensions/nice-errors/translations.yml +++ b/src/extensions/nice_errors/translations.yml @@ -26,6 +26,14 @@ strings: it: Ops! Non ho i permessi necessari per farlo. es-ES: ¡Ups! No tengo el permiso necesario para hacer eso. ru: Упс! У меня нет необходимых прав для выполнения этого действия. + error_cooldown_exceeded: + en-US: Whoops! You're doing that too fast. Please wait before trying again. + de: Hoppla! Du machst das zu schnell. Bitte warte, bevor du es erneut versuchst. + nl: Oeps! Je doet dat te snel. Wacht even voordat je het opnieuw probeert. + fr: Oups ! Vous faites cela trop vite. Veuillez attendre avant de réessayer. + it: Ops! Stai facendo troppo in fretta. Attendi prima di riprovare. + es-ES: ¡Ups! Estás haciendo eso demasiado rápido. Por favor, espera antes de intentarlo de nuevo. + ru: Упс! Вы делаете это слишком быстро. Пожалуйста, подождите, прежде чем попробовать снова. error_generic: en-US: Whoops! An error occurred while executing this command. de: Hoppla! Bei der Ausführung dieses Kommandos ist ein Fehler aufgetreten. diff --git a/src/extensions/ping/__init__.py b/src/extensions/ping/__init__.py index 2d6ba31..62965a7 100644 --- a/src/extensions/ping/__init__.py +++ b/src/extensions/ping/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .ping import setup, setup_webserver, on_startup, default, schema +from .ping import default, on_startup, schema, setup, setup_webserver -__all__ = ["setup", "setup_webserver", "on_startup", "default", "schema"] +__all__ = ["default", "on_startup", "schema", "setup", "setup_webserver"] diff --git a/src/extensions/ping/ping.py b/src/extensions/ping/ping.py index 31b2633..d6bb05a 100644 --- a/src/extensions/ping/ping.py +++ b/src/extensions/ping/ping.py @@ -1,15 +1,15 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import discord import aiohttp - +import discord +from discord.ext import bridge, commands from quart import Quart -from discord.ext import commands from schema import Schema -from src.log import logger + from src import custom -from discord.ext import bridge +from src.log import logger +from src.utils.cooldown import BucketType, cooldown default = { "enabled": True, @@ -18,76 +18,55 @@ schema = Schema( { "enabled": bool, - } + }, ) -class Ping(commands.Cog): - def __init__(self, bot: custom.Bot): - self.bot = bot - - @discord.slash_command(name="ping") - async def ping( - self, - ctx: custom.ApplicationContext, - ephemeral: bool = False, - use_embed: bool = False, - ): - await ctx.defer(ephemeral=ephemeral) - if use_embed: - embed = discord.Embed( - title="Pong!", - description=ctx.translations.response.format( - latency=round(self.bot.latency * 1000) - ), - color=discord.Colour.blurple(), - ) - return await ctx.respond(embed=embed, ephemeral=ephemeral) - return await ctx.respond( - f"Pong! {round(self.bot.latency * 1000)}ms", ephemeral=ephemeral - ) - - class BridgePing(commands.Cog): - def __init__(self, bot: custom.Bot): + def __init__(self, bot: custom.Bot) -> None: self.bot = bot @bridge.bridge_command() + @cooldown( + key="ping", + limit=1, + per=5, + strong=True, + bucket_type=BucketType.USER, + ) async def ping( self, - ctx: "custom.Context", + ctx: custom.Context, + *, ephemeral: bool = False, use_embed: bool = False, - ): + ) -> None: await ctx.defer(ephemeral=ephemeral) if use_embed: embed = discord.Embed( title="Pong!", - description=ctx.translations.response.format( - latency=round(self.bot.latency * 1000) - ), + description=ctx.translations.response.format(latency=round(self.bot.latency * 1000)), color=discord.Colour.blurple(), ) - return await ctx.respond(embed=embed, ephemeral=ephemeral) - return await ctx.respond( - f"Pong! {round(self.bot.latency * 1000)}ms", ephemeral=ephemeral - ) + await ctx.respond(embed=embed, ephemeral=ephemeral) + return + await ctx.respond(f"Pong! {round(self.bot.latency * 1000)}ms", ephemeral=ephemeral) -def setup(bot: custom.Bot): - # bot.add_cog(Ping(bot)) +def setup(bot: custom.Bot) -> None: bot.add_cog(BridgePing(bot)) -def setup_webserver(app: Quart, bot: discord.Bot): +def setup_webserver(app: Quart, bot: discord.Bot) -> None: @app.route("/ping") - async def ping(): + async def ping() -> dict[str, str]: # pyright: ignore[reportUnusedFunction] + if not bot.user: + return {"message": "Bot is offline"} bot_name = bot.user.name return {"message": f"{bot_name} is online"} -async def on_startup(config: dict): - async with aiohttp.ClientSession() as session: - async with session.get("https://httpbin.org/user-agent") as resp: - logger.info(f"HTTPBin user-agent: {await resp.text()}") - logger.info(f"Ping extension config: {config}") +async def on_startup(config: dict[str, bool]) -> None: + async with aiohttp.ClientSession() as session, session.get("https://httpbin.org/user-agent") as resp: + logger.info(f"HTTPBin user-agent: {await resp.text()}") + logger.info(f"Ping extension config: {config}") diff --git a/src/extensions/ping/readme.md b/src/extensions/ping/readme.md index 6016bef..247a67d 100644 --- a/src/extensions/ping/readme.md +++ b/src/extensions/ping/readme.md @@ -1,19 +1,20 @@ # Ping Extension -The Ping extension is a straightforward, yet essential part of your Botkit. -It doesn't require any configuration and is **enabled** by default. +The Ping extension is a straightforward, yet essential part of your Botkit. It doesn't +require any configuration and is **enabled** by default. ## Features -The Ping extension adds a `/ping` command to your bot. -When this command is invoked, the bot responds with a message indicating that it is online and operational. -This can be useful for quickly checking if your bot is responsive. -The Ping extension also serves an http endpoint at `/ping` that responds with a `200 OK` status code and the bot's name. +The Ping extension adds a `/ping` command to your bot. When this command is invoked, the +bot responds with a message indicating that it is online and operational. This can be +useful for quickly checking if your bot is responsive. The Ping extension also serves an +http endpoint at `/ping` that responds with a `200 OK` status code and the bot's name. ## Usage -To use the Ping extension type the `/ping` command. The bot should respond with a message, confirming its online status. -You can also send a `GET` request to the `/ping` endpoint to check if the bot is online. +To use the Ping extension type the `/ping` command. The bot should respond with a +message, confirming its online status. You can also send a `GET` request to the `/ping` +endpoint to check if the bot is online. ```bash curl http://localhost:5000/ping @@ -21,8 +22,11 @@ curl http://localhost:5000/ping ## Configuration -The Ping extension does not require any configuration. It is enabled by default. If you wish to disable it, you can do so by setting its `enabled` key to `false` in the `config.yml` file or through environment variables. +The Ping extension does not require any configuration. It is enabled by default. If you +wish to disable it, you can do so by setting its `enabled` key to `false` in the +`config.yml` file or through environment variables. ## Contributing -If you wish to contribute to the development of the Ping extension, please feel free to submit a pull request. We appreciate your help in making this extension better. +If you wish to contribute to the development of the Ping extension, please feel free to +submit a pull request. We appreciate your help in making this extension better. diff --git a/src/extensions/status-post/__init__.py b/src/extensions/status-post/__init__.py index 9c44609..191ddd8 100644 --- a/src/extensions/status-post/__init__.py +++ b/src/extensions/status-post/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .main import setup, default, schema +from .main import default, schema, setup -__all__ = ["setup", "default", "schema"] +__all__ = ["default", "schema", "setup"] diff --git a/src/extensions/status-post/main.py b/src/extensions/status-post/main.py index 64748e4..77fd518 100644 --- a/src/extensions/status-post/main.py +++ b/src/extensions/status-post/main.py @@ -1,14 +1,15 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import discord -import aiohttp +import math +from typing import Any -from discord.ext import commands +import aiohttp +import discord +from discord.ext import commands, tasks from schema import Schema -from src.log import logger -from discord.ext import tasks +from src.log import logger default = { "enabled": False, @@ -21,35 +22,36 @@ "enabled": bool, "url": str, "every": int, - } + }, ) class Status(commands.Cog): - def __init__(self, bot: discord.Bot, config: dict): - self.bot = bot - self.config = config - self.push_status_loop = tasks.loop(seconds=self.config["every"])( - self.push_status_loop - ) + def __init__(self, bot: discord.Bot, config: dict[Any, Any]) -> None: + self.bot: discord.Bot = bot + self.config: dict[Any, Any] = config + self.push_status_loop: tasks.Loop = tasks.loop(seconds=self.config["every"])(self.push_status_loop_meth) # pyright: ignore [reportMissingTypeArgument] @commands.Cog.listener(once=True) - async def on_ready(self): + async def on_ready(self) -> None: self.push_status_loop.start() - async def push_status_loop(self): + async def push_status_loop_meth(self) -> None: try: await self.push_status() logger.info("Pushed status.") - except Exception as e: - logger.error(f"Failed to push status: {e}") + except Exception: # noqa: BLE001 + logger.exception("Failed to push status.") - async def push_status(self): - ping = str(round(self.bot.latency * 1000)) - async with aiohttp.ClientSession() as session: - async with session.get(self.config["url"] + ping) as resp: - resp.raise_for_status() + async def push_status(self) -> None: + latency = self.bot.latency + if latency == float("inf") or math.isnan(latency): + logger.warning("Latency is infinite or NaN, skipping status push.") + return + ping = str(round(latency * 1000)) + async with aiohttp.ClientSession() as session, session.get(self.config["url"] + ping) as resp: + resp.raise_for_status() -def setup(bot: discord.Bot, config: dict): +def setup(bot: discord.Bot, config: dict[Any, Any]) -> None: bot.add_cog(Status(bot, config)) diff --git a/src/extensions/status-post/readme.md b/src/extensions/status-post/readme.md index f8c5706..12183c2 100644 --- a/src/extensions/status-post/readme.md +++ b/src/extensions/status-post/readme.md @@ -1,16 +1,18 @@ # Status Extension -The Status extension is a straightforward, yet essential part of your Botkit. -It requires minimal configuration and can be enabled or disabled as needed. +The Status extension is a straightforward, yet essential part of your Botkit. It +requires minimal configuration and can be enabled or disabled as needed. ## Features -The Status extension periodically pushes the bot's status to a specified URL. -This can be useful for monitoring the bot's health and responsiveness. +The Status extension periodically pushes the bot's status to a specified URL. This can +be useful for monitoring the bot's health and responsiveness. ## Usage -To use the Status extension, configure the `url` and `every` keys in the `config.yml` file or through environment variables. The bot will push its status to the specified URL at the configured interval. +To use the Status extension, configure the `url` and `every` keys in the `config.yml` +file or through environment variables. The bot will push its status to the specified URL +at the configured interval. ## Configuration @@ -30,4 +32,6 @@ status: ``` ## Contributing -If you wish to contribute to the development of the Status extension, please feel free to submit a pull request. We appreciate your help in making this extension better. \ No newline at end of file + +If you wish to contribute to the development of the Status extension, please feel free +to submit a pull request. We appreciate your help in making this extension better. diff --git a/src/i18n/__init__.py b/src/i18n/__init__.py index fa34828..f52ffb7 100644 --- a/src/i18n/__init__.py +++ b/src/i18n/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .utils import apply, load_translation from .classes import apply_locale +from .utils import apply, load_translation -__all__ = ["apply", "load_translation", "apply_locale"] +__all__ = ["apply", "apply_locale", "load_translation"] diff --git a/src/i18n/classes.py b/src/i18n/classes.py index 099c1ab..32bbf85 100644 --- a/src/i18n/classes.py +++ b/src/i18n/classes.py @@ -1,8 +1,9 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from typing import Any -from typing_extensions import override +from collections.abc import Generator, Iterator +from typing import Any, Self, override + from pydantic import BaseModel, Field LOCALES = ( @@ -42,11 +43,11 @@ class RawTranslation(BaseModel): - en_US: str | None = Field(None, alias="en-US") - en_GB: str | None = Field(None, alias="en-GB") + en_US: str | None = Field(None, alias="en-US") # noqa: N815 + en_GB: str | None = Field(None, alias="en-GB") # noqa: N815 bg: str | None = None - zh_CN: str | None = Field(None, alias="zh-CN") - zh_TW: str | None = Field(None, alias="zh-TW") + zh_CN: str | None = Field(None, alias="zh-CN") # noqa: N815 + zh_TW: str | None = Field(None, alias="zh-TW") # noqa: N815 hr: str | None = None cs: str | None = None da: str | None = None @@ -63,12 +64,12 @@ class RawTranslation(BaseModel): lt: str | None = None no: str | None = None pl: str | None = None - pt_BR: str | None = Field(None, alias="pt-BR") + pt_BR: str | None = Field(None, alias="pt-BR") # noqa: N815 ro: str | None = None ru: str | None = None - es_ES: str | None = Field(None, alias="es-ES") + es_ES: str | None = Field(None, alias="es-ES") # noqa: N815 es_419: str | None = Field(None, alias="es-419") - sv_SE: str | None = Field(None, alias="sv-SE") + sv_SE: str | None = Field(None, alias="sv-SE") # noqa: N815 th: str | None = None tr: str | None = None uk: str | None = None @@ -79,33 +80,89 @@ class Config: class Translation(BaseModel): - def get_for_locale(self, locale: str) -> "TranslationWrapper": + def get_for_locale(self, locale: str) -> "TranslationWrapper[Self]": return apply_locale(self, locale) -class TranslationWrapper: - def __init__(self, model: "Translatable", locale: str, default: str = DEFAULT): - self._model = model +class TranslationWrapper[T: "Translatable"]: + def __init__(self, model: "Translatable", locale: str, default: str = DEFAULT) -> None: + self._model: T = model self._default: str self.default = default.replace("-", "_") self._locale: str self.locale = locale.replace("-", "_") + def _wrap_value(self, value: Any) -> Any: + """Consistently wrap values in TranslationWrapper if needed.""" + if value is None: + return None + if isinstance(value, str | int | float | bool): + return value + if isinstance(value, RawTranslation): + try: + return getattr(value, self._locale) or getattr(value, self._default) + except AttributeError: + return getattr(value, self._default) + if isinstance(value, list | tuple): + return [self._wrap_value(item) for item in value] + + # For any other type (including Pydantic models), apply locale + return apply_locale(value, self._locale) + def __getattr__(self, key: str) -> Any: if isinstance(self._model, dict): - applicable = self._model.get(key) - if not applicable: + if key not in self._model: raise AttributeError(f'Key "{key}" not found in {self._model}') + value = self._model[key] else: - applicable = getattr(self._model, key) - if isinstance(applicable, RawTranslation): - try: - return getattr(applicable, self._locale) or getattr( - applicable, self._default - ) - except AttributeError: - return getattr(applicable, self._default) - return apply_locale(applicable, self._locale) + value = getattr(self._model, key) + return self._wrap_value(value) + + def __getitem__(self, item: Any) -> Any: + if isinstance(self._model, dict): + if not isinstance(item, str): + raise TypeError(f"Key must be a string, not {type(item).__name__}") + return self.__getattr__(item) + if isinstance(self._model, list): + if not isinstance(item, int): + raise TypeError(f"Index must be an integer, not {type(item).__name__}") + return self._wrap_value(self._model[item]) + return self.__getattr__(item) + + def keys(self) -> Generator[str, None, None]: + if not isinstance(self._model, dict): + raise TypeError(f"Cannot get keys from {type(self._model).__name__}") + yield from self._model.keys() + + def items(self) -> Generator[tuple[str, Any], None, None]: + if isinstance(self._model, dict): + for key, value in self._model.items(): + yield key, self._wrap_value(value) + else: + for key in self.keys(): + yield key, self._wrap_value(getattr(self._model, key)) + + def values(self) -> Generator[Any, None, None]: + if isinstance(self._model, dict): + for value in self._model.values(): + yield self._wrap_value(value) + else: + for key in self.keys(): + yield self._wrap_value(getattr(self._model, key)) + + def __iter__(self) -> Iterator[Any]: + if isinstance(self._model, list): + for item in self._model: + yield self._wrap_value(item) + else: + yield from self.keys() + + def __len__(self) -> int: + if isinstance(self._model, dict): + return len(self._model) + if isinstance(self._model, list): + return len(self._model) + return len(self._model.__dict__) @property def locale(self) -> str: @@ -131,7 +188,7 @@ def default(self, value: str) -> None: @override def __repr__(self) -> str: - return repr(self._model) + return f"TranslationWrapper({self._model!r}, locale={self._locale!r}, default={self._default!r})" @override def __str__(self) -> str: @@ -162,9 +219,7 @@ class Deg1CommandTranslation(CommandTranslation): commands: dict[str, Deg2CommandTranslation] | None = None -AnyCommandTranslation = ( - Deg1CommandTranslation | Deg2CommandTranslation | Deg3CommandTranslation -) +AnyCommandTranslation = Deg1CommandTranslation | Deg2CommandTranslation | Deg3CommandTranslation class ExtensionTranslation(Translation): @@ -172,11 +227,11 @@ class ExtensionTranslation(Translation): strings: dict[str, RawTranslation] | None = None -def apply_locale( - model: "Translatable | TranslationWrapper", +def apply_locale[T: "Translatable"]( + model: T, locale: str | None, default: str | None = DEFAULT, -) -> TranslationWrapper: +) -> TranslationWrapper[T]: default = default if default is not None else DEFAULT if locale is None: locale = DEFAULT diff --git a/src/i18n/utils.py b/src/i18n/utils.py index 1efa633..15630e8 100644 --- a/src/i18n/utils.py +++ b/src/i18n/utils.py @@ -1,17 +1,19 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from typing import TypeVar, TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar + import discord +import yaml +from discord.ext import commands as prefixed + +from src.log import logger as main_logger from .classes import ( - ExtensionTranslation, Deg1CommandTranslation, Deg2CommandTranslation, + ExtensionTranslation, ) -from src.log import logger as main_logger -import yaml -from discord.ext import commands as prefixed if TYPE_CHECKING: from src import custom @@ -26,10 +28,13 @@ def remove_none(d: dict[T, V]) -> dict[T, V]: """Remove None values from a dictionary. Args: + ---- d (dict[T, V]): The dictionary to remove None values from. Returns: + ------- dict[T, V]: The dictionary without None values. + """ return {k: v for k, v in d.items() if v is not None} @@ -40,17 +45,19 @@ def merge_command_translations( """Merge command translations into a single dictionary. Args: + ---- translations (list[ExtensionTranslation]): A list of translations. Returns: + ------- dict[str, Deg1CommandTranslation] | None: A dictionary of command translations. Raises: + ------ None + """ - command_translations: list[dict[str, Deg1CommandTranslation]] = [ - t.commands for t in translations if t.commands - ] + command_translations: list[dict[str, Deg1CommandTranslation]] = [t.commands for t in translations if t.commands] if not command_translations: return None result: dict[str, Deg1CommandTranslation] = {} @@ -69,67 +76,62 @@ def merge_command_translations( discord.SlashCommand, discord.SlashCommandGroup, prefixed.Command, # pyright: ignore[reportMissingTypeArgument] + discord.MessageCommand, ) -def localize_commands( +def localize_commands( # noqa: PLR0912 commands: list[CommandT], translations: ExtensionTranslation | Deg1CommandTranslation | Deg2CommandTranslation | dict[str, Deg1CommandTranslation], - DEFAULT_LOCALE: str = "en-US", + default_locale: str = "en-US", ) -> tuple[int, int]: """Recursively localize commands and their subcommands. Args: + ---- commands: List of commands to localize. translations: Translations for the commands. - DEFAULT_LOCALE: The default locale to use. + default_locale: The default locale to use. Returns: + ------- None + """ logger.info("Localizing commands...") err = 0 tot = 0 for command in commands: if isinstance( - command, (discord.SlashCommand, discord.SlashCommandGroup, prefixed.Command) + command, discord.SlashCommand | discord.SlashCommandGroup | prefixed.Command | discord.MessageCommand ): tot += 1 try: try: - if isinstance(translations, dict): - translatable = translations - else: - translatable = translations.commands + translatable = translations if isinstance(translations, dict) else translations.commands if translatable: translation = translatable.get(command.name) if not translation: - raise AttributeError + raise AttributeError # noqa: TRY301 else: - raise AttributeError + raise AttributeError # noqa: TRY301 except AttributeError: - logger.warning( - f"Command /{command.qualified_name} is not defined in translations, continuing..." - ) + logger.warning(f"Command /{command.qualified_name} is not defined in translations, continuing...") err += 1 continue if translation.name: name = remove_none(translation.name.model_dump(by_alias=True)) - command.name = name.get(DEFAULT_LOCALE, command.name) + command.name = name.get(default_locale, command.name) if not isinstance(command, prefixed.Command): command.name_localizations = name if translation.description: - description = remove_none( - translation.description.model_dump(by_alias=True) - ) - command.description = description.get( - DEFAULT_LOCALE, command.description - ) + description = remove_none(translation.description.model_dump(by_alias=True)) + command.description = description.get(default_locale, command.description) if not isinstance(command, prefixed.Command): - command.description_localizations = description + command.description_localizations = description # pyright: ignore [reportAttributeAccessIssue] if translation.strings: command.translations = translation.strings # pyright: ignore[reportAttributeAccessIssue] if isinstance(command, discord.SlashCommand) and translation.options: @@ -138,26 +140,23 @@ def localize_commands( opt = translation.options[option.name] if opt.name: name = remove_none(opt.name.model_dump(by_alias=True)) - option.name = name.get(DEFAULT_LOCALE, option.name) + option.name = name.get(default_locale, option.name) option.name_localizations = name if opt.description: - description = remove_none( - opt.description.model_dump(by_alias=True) - ) - option.description = description.get( - DEFAULT_LOCALE, option.description - ) + description = remove_none(opt.description.model_dump(by_alias=True)) + option.description = description.get(default_locale, option.description) option.description_localizations = description else: logger.warning( - f"Option {option.name} of command /{command.qualified_name} is not defined in translations, continuing..." + f"Option {option.name} of command /{command.qualified_name} is not defined in translations, continuing...", # noqa: E501 ) if isinstance(command, discord.SlashCommandGroup) and isinstance( - translation, (Deg1CommandTranslation, Deg2CommandTranslation) + translation, + Deg1CommandTranslation | Deg2CommandTranslation, ): - localize_commands(command.subcommands, translation, DEFAULT_LOCALE) - except Exception as e: - logger.error(f"Error localizing command /{command.name}: {e}") + localize_commands(command.subcommands, translation, default_locale) + except Exception: + logger.exception(f"Error localizing command /{command.name}") err += 1 return err, tot @@ -166,16 +165,19 @@ def load_translation(path: str) -> ExtensionTranslation: """Load a translation from a file. Args: + ---- path (str): The path to the translation file. Returns: + ------- ExtensionTranslation: The loaded translation. Raises: + ------ yaml.YAMLError: If the file is not a valid YAML file. - """ - with open(path, "r", encoding="utf-8") as f: + """ + with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) return ExtensionTranslation(**data) @@ -183,17 +185,20 @@ def load_translation(path: str) -> ExtensionTranslation: def apply( bot: "custom.Bot", translations: list[ExtensionTranslation], - DEFAULT_LOCALE: str = "en-US", + default_locale: str = "en-US", ) -> None: """Apply translations to the bot. Args: + ---- bot: The bot to apply translations to. translations: The translations to apply. - DEFAULT_LOCALE: The default locale to use. + default_locale: The default locale to use. Returns: + ------- None + """ logger.info("Applying translations") command_translations = merge_command_translations(translations) @@ -204,7 +209,7 @@ def apply( err, tot = localize_commands( [*bot.pending_application_commands, *bot.commands], # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType] command_translations, - DEFAULT_LOCALE, + default_locale, ) if tot == err: logger.error(f"Localized {tot - err}/{tot} commands.") diff --git a/src/log/logger.py b/src/log/logger.py index 8f590e5..da6678d 100644 --- a/src/log/logger.py +++ b/src/log/logger.py @@ -4,6 +4,8 @@ import logging import os import time +from typing import Any + import coloredlogs from src.config import config @@ -14,7 +16,7 @@ class CustomLogger(logging.Logger): - def success(self, msg, *args, **kwargs) -> None: # pyright: ignore[reportUnknownParameterType,reportMissingParameterType] + def success(self, msg: str, *args: Any, **kwargs: Any) -> None: # pyright: ignore[reportUnknownParameterType,reportMissingParameterType] if self.isEnabledFor(SUCCESS): self._log(SUCCESS, msg, args, **kwargs) # pyright: ignore[reportUnknownArgumentType] @@ -22,18 +24,12 @@ def success(self, msg, *args, **kwargs) -> None: # pyright: ignore[reportUnknow # Register the custom logger class logging.setLoggerClass(CustomLogger) -level: int = getattr( - logging, config.get("logging", {}).get("level", "").upper() or "INFO" -) +level: int = getattr(logging, config.get("logging", {}).get("level", "").upper() or "INFO") logging.basicConfig(level=level, handlers=[]) os.makedirs("logs", exist_ok=True) file_handler = logging.FileHandler(f"logs/{time.time()}.log", encoding="utf-8") -file_handler.setFormatter( - logging.Formatter( - "%(levelname)-8s at %(asctime)s: %(message)s\n\t%(pathname)s:%(lineno)d" - ) -) +file_handler.setFormatter(logging.Formatter("%(levelname)-8s at %(asctime)s: %(message)s\n\t%(pathname)s:%(lineno)d")) file_handler.setLevel("DEBUG") # More stylish coloredlogs format diff --git a/src/patcher.py b/src/patcher.py new file mode 100644 index 0000000..1571c63 --- /dev/null +++ b/src/patcher.py @@ -0,0 +1,34 @@ +# Copyright (c) NiceBots.xyz +# SPDX-License-Identifier: MIT + +import os +import sys +from typing import Any + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +# the above line allows us to import from src without any issues whilst using src/__main__.py +import importlib.util +from glob import iglob + +from src.config import config +from src.log import logger +from src.utils.setup_func import setup_func + + +async def load_and_run_patches() -> None: + for patch_file in iglob("src/extensions/*/patch.py"): + extension = os.path.basename(os.path.dirname(patch_file)) + its_config: dict[Any, Any] = {} + if its_config := ( + config["extensions"].get(extension, config["extensions"].get(extension.replace("_", "-"), {})) + ): + if not its_config.get("enabled", False): + continue + logger.info(f"Loading patch for extension {extension}") + spec = importlib.util.spec_from_file_location(f"src.extensions.{extension}.patch", patch_file) + if not spec or not spec.loader: + continue + patch_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(patch_module) + if hasattr(patch_module, "patch") and callable(patch_module.patch): + await setup_func(patch_module.patch, config=its_config) diff --git a/src/start.py b/src/start.py index 7d4b02c..9c9bfda 100644 --- a/src/start.py +++ b/src/start.py @@ -1,46 +1,57 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -import discord +import asyncio import importlib import importlib.util -import asyncio +from glob import iglob +from os.path import basename, splitext +from typing import TYPE_CHECKING, Any, TypedDict -from discord.ext import commands +import discord import yaml +from discord.errors import LoginFailure +from discord.ext import commands from quart import Quart -from glob import iglob + +from src import custom, i18n +from src.config import config from src.i18n.classes import ExtensionTranslation -from src.config import config, store_config from src.log import logger, patch -from os.path import splitext, basename -from types import ModuleType -from typing import Any, Callable, TypedDict, TYPE_CHECKING -from collections.abc import Coroutine -from src.utils import validate_module, unzip_extensions, setup_func -from src import i18n +from src.utils import setup_func, unzip_extensions, validate_module from src.utils.iterator import next_default -from src import custom if TYPE_CHECKING: - FunctionConfig = TypedDict("FunctionConfig", {"enabled": bool}) + from collections.abc import Callable, Coroutine + from types import ModuleType + + class FunctionConfig(TypedDict): + enabled: bool + FunctionlistType = list[tuple[Callable[..., Any], FunctionConfig]] -async def start_bot(bot: custom.Bot, token: str): - await bot.start(token) +async def start_bot(bot: custom.Bot, token: str) -> None: + try: + await bot.start(token) + except LoginFailure as e: + logger.critical("Failed to log in, is the bot token valid?") + logger.debug("", exc_info=e) + except Exception as e: # noqa: BLE001 + logger.critical("An unexpected error occurred while starting the bot.") + logger.debug("", exc_info=e) -async def start_backend(app: Quart, bot: discord.Bot, token: str): +async def start_backend(app: Quart, bot: discord.Bot, token: str) -> None: + from hypercorn.asyncio import serve # pyright: ignore [reportUnknownVariableType] from hypercorn.config import Config from hypercorn.logging import Logger as HypercornLogger - from hypercorn.asyncio import serve # pyright: ignore [reportUnknownVariableType] class CustomLogger(HypercornLogger): def __init__( self, - *args, # pyright: ignore [reportUnknownParameterType,reportMissingParameterType] - **kwargs, # pyright: ignore [reportUnknownParameterType,reportMissingParameterType] + *args, # pyright: ignore [reportUnknownParameterType,reportMissingParameterType] # noqa: ANN002 + **kwargs, # pyright: ignore [reportUnknownParameterType,reportMissingParameterType] # noqa: ANN003 ) -> None: super().__init__( *args, # pyright: ignore [reportUnknownArgumentType] @@ -56,28 +67,44 @@ def __init__( app_config.logger_class = CustomLogger app_config.include_server_header = False # security app_config.bind = ["0.0.0.0:5000"] - await bot.login(token) - await serve(app, app_config) - patch("hypercorn.error") - - -def load_extensions() -> ( - tuple[ - "FunctionlistType", - "FunctionlistType", - "FunctionlistType", - "list[ExtensionTranslation]", - ] -): - bot_functions: "FunctionlistType" = [] - back_functions: "FunctionlistType" = [] - startup_functions: "FunctionlistType" = [] + try: + await bot.login(token) + await serve(app, app_config) + patch("hypercorn.error") + except Exception as e: # noqa: BLE001 + logger.critical("An error occurred while starting the backend server.") + logger.debug("", exc_info=e) + + +def load_extensions() -> tuple[ + "FunctionlistType", + "FunctionlistType", + "FunctionlistType", + "list[ExtensionTranslation]", +]: + """Load extensions from the extensions directory. + + Returns: + tuple[FunctionlistType, FunctionlistType, FunctionlistType, list[ExtensionTranslation]]: A tuple containing + the bot functions, backend functions, startup functions, and translations. + + """ + bot_functions: FunctionlistType = [] + back_functions: FunctionlistType = [] + startup_functions: FunctionlistType = [] translations: list[ExtensionTranslation] = [] for extension in iglob("src/extensions/*"): name = splitext(basename(extension))[0] + if name.endswith(("_", "_/", ".py")): + continue - its_config = config["extensions"].get(name, {}) - module: ModuleType = importlib.import_module(f"src.extensions.{name}") + its_config = config["extensions"].get(name, config["extensions"].get(name.replace("_", "-"), {})) + try: + module: ModuleType = importlib.import_module(f"src.extensions.{name}") + except ImportError as e: + logger.error(f"Failed to import extension {name}") + logger.debug("", exc_info=e) + continue if not its_config: its_config = module.default config["extensions"][name] = its_config @@ -112,16 +139,18 @@ async def setup_and_start_bot( bot_functions: "FunctionlistType", translations: list[ExtensionTranslation], config: dict[str, Any], -): +) -> None: intents = discord.Intents.default() if config.get("prefix"): intents.message_content = True + # Get cache configuration + cache_config = config.get("cache", {}) bot = custom.Bot( intents=intents, help_command=None, - command_prefix=( - config.get("prefix", {}).get("prefix") or commands.when_mentioned - ), + command_prefix=(config.get("prefix", {}).get("prefix") or commands.when_mentioned), + cache_type=cache_config.get("type", "memory"), + cache_config=cache_config.get("redis"), ) for function, its_config in bot_functions: setup_func(function, bot=bot, config=its_config) @@ -129,13 +158,13 @@ async def setup_and_start_bot( if not config.get("prefix", {}).get("enabled", True): bot.prefixed_commands = {} if not config.get("slash", {}).get("enabled", True): - bot._pending_application_commands = [] # pyright: ignore[reportPrivateUsage] + bot._pending_application_commands = [] # pyright: ignore[reportPrivateUsage] # noqa: SLF001 await start_bot(bot, config["token"]) async def setup_and_start_backend( back_functions: "FunctionlistType", -): +) -> None: back_bot = discord.Bot(intents=discord.Intents.default()) app = Quart("backend") for function, its_config in back_functions: @@ -147,44 +176,41 @@ async def run_startup_functions( startup_functions: "FunctionlistType", app: Quart | None, back_bot: discord.Bot | None, -): +) -> None: startup_coros = [ - setup_func(function, app=app, bot=back_bot, config=its_config) - for function, its_config in startup_functions + setup_func(function, app=app, bot=back_bot, config=its_config) for function, its_config in startup_functions ] await asyncio.gather(*startup_coros) -async def main(run_bot: bool | None = None, run_backend: bool | None = None): - assert config.get("bot", {}).get("token"), "No bot token provided in config" +async def start(run_bot: bool | None = None, run_backend: bool | None = None) -> None: + if not config.get("bot", {}).get("token"): + logger.critical("No bot token provided in config, exiting...") + return + if config.get("db", {}).get("enabled", False): + from src.database.config import init + + logger.info("Initializing database...") + await init() + unzip_extensions() run_bot = run_bot if run_bot is not None else config.get("use", {}).get("bot", True) - run_backend = ( - run_backend - if run_backend is not None - else config.get("use", {}).get("backend", True) - ) + run_backend = run_backend if run_backend is not None else config.get("use", {}).get("backend", True) bot_functions, back_functions, startup_functions, translations = load_extensions() coros: list[Coroutine[Any, Any, Any]] = [] if bot_functions and run_bot: - coros.append( - setup_and_start_bot(bot_functions, translations, config.get("bot", {})) - ) + coros.append(setup_and_start_bot(bot_functions, translations, config.get("bot", {}))) if back_functions and run_backend: coros.append(setup_and_start_backend(back_functions)) - assert coros, "No extensions to run" + if not coros: + logger.error("No extensions to run, exiting...") + return if startup_functions: app = Quart("backend") if (back_functions and run_backend) else None - back_bot = ( - discord.Bot(intents=discord.Intents.default()) - if (back_functions and run_backend) - else None - ) + back_bot = discord.Bot(intents=discord.Intents.default()) if (back_functions and run_backend) else None await run_startup_functions(startup_functions, app, back_bot) await asyncio.gather(*coros) - - store_config() diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 75bfb55..67d0d26 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,8 +1,8 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from .extensions import validate_module, unzip_extensions +from .extensions import unzip_extensions, validate_module from .misc import mention_command from .setup_func import setup_func -__all__ = ["validate_module", "unzip_extensions", "mention_command", "setup_func"] +__all__ = ["mention_command", "setup_func", "unzip_extensions", "validate_module"] diff --git a/src/utils/cooldown.py b/src/utils/cooldown.py new file mode 100644 index 0000000..dc8779e --- /dev/null +++ b/src/utils/cooldown.py @@ -0,0 +1,123 @@ +# Copyright (c) NiceBots +# SPDX-License-Identifier: MIT + +import time +from collections.abc import Awaitable, Callable, Coroutine +from enum import Enum +from functools import wraps +from inspect import isawaitable +from typing import Any, Concatenate, cast + +import discord +from discord.ext import commands + +from src import custom + +type ReactiveCooldownSetting[T: Any] = T | Callable[[custom.Bot, custom.Context], T | Coroutine[Any, Any, T]] +type CogCommandFunction[T: commands.Cog, **P] = Callable[Concatenate[T, P], Awaitable[None]] + + +class BucketType(Enum): + DEFAULT = "default" # Uses provided key as is + USER = "user" # Per-user cooldown + MEMBER = "member" # Per-member (user+guild) cooldown + GUILD = "guild" # Per-guild cooldown + CHANNEL = "channel" # Per-channel cooldown + CATEGORY = "category" # Per-category cooldown + ROLE = "role" # Per-role cooldown (uses highest role) + + +async def parse_reactive_setting[T](value: ReactiveCooldownSetting[T], bot: custom.Bot, ctx: custom.Context) -> T: + if isinstance(value, type): + return value # pyright: ignore [reportReturnType] + if callable(value): + value = value(bot, ctx) # pyright: ignore [reportAssignmentType] + if isawaitable(value): + value = await value + return value # pyright: ignore [reportReturnType] + + +class CooldownExceeded(commands.CheckFailure): + def __init__(self, retry_after: float, bucket_type: BucketType) -> None: + self.retry_after: float = retry_after + self.bucket_type: BucketType = bucket_type + super().__init__(f"You are on {bucket_type.value} cooldown") + + +def get_bucket_key(ctx: custom.Context, base_key: str, bucket_type: BucketType) -> str: # noqa: PLR0911 + """Generate a cooldown key based on the bucket type.""" + match bucket_type: + case BucketType.USER: + return f"{base_key}:user:{ctx.author.id}" + case BucketType.MEMBER: + return f"{base_key}:member:{ctx.guild}:{ctx.author.id}" if ctx.guild else f"{base_key}:user:{ctx.author.id}" + case BucketType.GUILD: + return f"{base_key}:guild:{ctx.guild.id}" if ctx.guild else base_key + case BucketType.CHANNEL: + return f"{base_key}:channel:{ctx.channel.id}" + case BucketType.CATEGORY: + category_id = ctx.channel.category_id if hasattr(ctx.channel, "category_id") else None + return f"{base_key}:category:{category_id}" if category_id else f"{base_key}:channel:{ctx.channel.id}" + case BucketType.ROLE: + if ctx.guild and hasattr(ctx.author, "roles") and isinstance(ctx.author, discord.Member): + top_role_id = max((role.id for role in ctx.author.roles), default=0) + return f"{base_key}:role:{top_role_id}" + return f"{base_key}:user:{ctx.author.id}" + case _: # BucketType.DEFAULT + return base_key + + +def cooldown[C: commands.Cog, **P]( + key: ReactiveCooldownSetting[str], + *, + limit: ReactiveCooldownSetting[int], + per: ReactiveCooldownSetting[int], + bucket_type: ReactiveCooldownSetting[BucketType] = BucketType.DEFAULT, + strong: ReactiveCooldownSetting[bool] = False, + cls: ReactiveCooldownSetting[type[CooldownExceeded]] = CooldownExceeded, +) -> Callable[[CogCommandFunction[C, P]], CogCommandFunction[C, P]]: + """Enhanced cooldown decorator that supports different bucket types. + + Args: + key: Base key for the cooldown + limit: Number of uses allowed + per: Time period in seconds + bucket_type: Type of bucket to use for the cooldown + strong: If True, adds current timestamp even if limit is reached + cls: Custom exception class to raise + + """ + + def inner(func: CogCommandFunction[C, P]) -> CogCommandFunction[C, P]: + @wraps(func) + async def wrapper(self: C, *args: P.args, **kwargs: P.kwargs) -> None: + ctx: custom.Context = args[0] # pyright: ignore [reportAssignmentType] + cache = ctx.bot.botkit_cache + key_value: str = await parse_reactive_setting(key, ctx.bot, ctx) + limit_value: int = await parse_reactive_setting(limit, ctx.bot, ctx) + per_value: int = await parse_reactive_setting(per, ctx.bot, ctx) + strong_value: bool = await parse_reactive_setting(strong, ctx.bot, ctx) + cls_value: type[CooldownExceeded] = await parse_reactive_setting(cls, ctx.bot, ctx) + bucket_type_value: BucketType = await parse_reactive_setting(bucket_type, ctx.bot, ctx) + + # Generate the full cooldown key based on bucket type + full_key = get_bucket_key(ctx, key_value, bucket_type_value) + + now = time.time() + time_stamps = cast(tuple[float, ...], await cache.get(full_key, default=(), namespace="cooldown")) + time_stamps = tuple(filter(lambda x: x > now - per_value, time_stamps)) + time_stamps = time_stamps[-limit_value:] + + if len(time_stamps) < limit_value or strong_value: + time_stamps = (*time_stamps, now) + await cache.set(full_key, time_stamps, namespace="cooldown", ttl=per_value) + limit_value += 1 # to account for the current command + + if len(time_stamps) >= limit_value: + raise cls_value(min(time_stamps) - now + per_value, bucket_type_value) + + await func(self, *args, **kwargs) + + return wrapper + + return inner diff --git a/src/utils/extensions.py b/src/utils/extensions.py index bec9262..acd5688 100644 --- a/src/utils/extensions.py +++ b/src/utils/extensions.py @@ -1,54 +1,52 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT - +# ruff: noqa: S101 import inspect import os -import zipfile -import discord import warnings - -from types import ModuleType -from typing import Any, Callable +import zipfile +from collections.abc import Callable from glob import iglob -from schema import Schema +from types import ModuleType +from typing import Any + +import discord from quart import Quart +from schema import Schema, SchemaError from src.log import logger -def check_typing(module: ModuleType, func: Callable, types: dict[str, Any]): +def check_typing(module: ModuleType, func: Callable, types: dict[str, Any]) -> None: signature = inspect.signature(func) for name, parameter in signature.parameters.items(): - if name in types and not parameter.annotation == types[name]: + if name in types and parameter.annotation != types[name]: warnings.warn( - f"Parameter {name} of function {func.__name__} of module {module.__name__} does not have the correct type annotation (is {parameter.annotation} should be {types[name]})" + f"Parameter {name} of function {func.__name__} of module {module.__name__} does not have the correct type annotation (is {parameter.annotation} should be {types[name]})", # noqa: E501 + stacklevel=1, ) -def check_func( - module: ModuleType, func: Callable, max_args: int, types: dict[str, Any] -): - assert callable( - func - ), f"Function {func.__name__} of module {module.__name__} is not callable" +def check_func(module: ModuleType, func: Callable, max_args: int, types: dict[str, Any]) -> None: + assert callable(func), f"Function {func.__name__} of module {module.__name__} is not callable" signature = inspect.signature(func) - assert ( - len(signature.parameters) <= max_args - ), f"Function {func.__name__} of module {module.__name__} has too many arguments" - assert all( - param in types for param in signature.parameters.keys() - ), f"Function {func.__name__} of module {module.__name__} does not accept the correct arguments ({', '.join(types.keys())})" - # check_typing(module, func, types) # temporarily disabled due to unwanted behavior + assert len(signature.parameters) <= max_args, ( + f"Function {func.__name__} of module {module.__name__} has too many arguments" + ) + assert all(param in types for param in signature.parameters), ( + f"Function {func.__name__} of module {module.__name__} does not accept the correct arguments" + "({', '.join(types.keys())})" + ) + # check_typing(module, func, types) # temporarily disabled due to unwanted behavior # noqa: ERA001 # noinspection DuplicatedCode def validate_module(module: ModuleType, config: dict[str, Any] | None = None) -> None: - """ - Validate the module to ensure it has the required functions and attributes to be loaded as an extension + """Validate the module to ensure it has the required functions and attributes to be loaded as an extension. + :param module: The module to validate - :param config: The configuration to validate against + :param config: The configuration to validate against. """ - if hasattr(module, "setup"): check_func(module, module.setup, 2, {"bot": discord.Bot, "config": dict}) @@ -60,7 +58,8 @@ def validate_module(module: ModuleType, config: dict[str, Any] | None = None) -> {"app": Quart, "bot": discord.Bot, "config": dict}, ) assert hasattr(module, "setup_webserver") or hasattr( - module, "setup" + module, + "setup", ), f"Extension {module.__name__} does not have a setup or setup_webserver function" if hasattr(module, "on_startup"): check_func( @@ -70,15 +69,18 @@ def validate_module(module: ModuleType, config: dict[str, Any] | None = None) -> {"app": Quart, "bot": discord.Bot, "config": dict}, ) - assert hasattr(module, "default") and isinstance( - module.default, dict - ), f"Extension {module.__name__} does not have a default configuration" - assert ( - "enabled" in module.default - ), f"Extension {module.__name__} does not have an enabled key in its default configuration" + assert hasattr(module, "default"), f"Extension {module.__name__} does not have a default configuration" + assert isinstance( + module.default, + dict, + ), f"Extension {module.__name__} has a default configuration of type {type(module.default)} instead of dict" + assert "enabled" in module.default, ( + f"Extension {module.__name__} does not have an enabled key in its default configuration" + ) if hasattr(module, "schema"): - assert ( - isinstance(module.schema, Schema) or isinstance(module.schema, dict) + assert isinstance( + module.schema, + Schema | dict, ), f"Extension {module.__name__} has a schema of type {type(module.schema)} instead of Schema or dict" if isinstance(module.schema, dict): @@ -88,15 +90,16 @@ def validate_module(module: ModuleType, config: dict[str, Any] | None = None) -> else: try: module.schema.validate(module.default) - except Exception as e: + except SchemaError as e: warnings.warn( - f"Default configuration for extension {module.__name__} does not match schema: {e}" + f"Default configuration for extension {module.__name__} does not match schema: {e}", + stacklevel=1, ) else: - warnings.warn(f"Extension {module.__name__} does not have a schema") + warnings.warn(f"Extension {module.__name__} does not have a schema", stacklevel=1) -def unzip_extensions(): +def unzip_extensions() -> None: for file in iglob("src/extensions/*.zip"): with zipfile.ZipFile(file, "r") as zip_ref: zip_ref.extractall("src/extensions") diff --git a/src/utils/iterator.py b/src/utils/iterator.py index ac43772..f5e9bad 100644 --- a/src/utils/iterator.py +++ b/src/utils/iterator.py @@ -1,8 +1,8 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from typing import TypeVar from collections.abc import Iterator +from typing import TypeVar T = TypeVar("T") V = TypeVar("V") diff --git a/src/utils/setup_func.py b/src/utils/setup_func.py index 0aa0e94..b3d6d3c 100644 --- a/src/utils/setup_func.py +++ b/src/utils/setup_func.py @@ -1,16 +1,17 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT +from collections.abc import Callable from inspect import signature -from typing import Any, Callable +from typing import Any def setup_func(func: Callable[..., Any], **kwargs: Any) -> Any: - """ - Setup a Coroutine function with the required arguments from the kwargs + """Set up a Coroutine function with the required arguments from the kwargs. + :param func: The function to setup :param kwargs: The arguments that may be passed to the function if the function requires them - :return: The result of the function + :return: The result of the function. """ parameters = signature(func).parameters func_kwargs = {} diff --git a/tests/__init__.py b/tests/__init__.py index 3fb26ca..8b45425 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,2 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT - diff --git a/tests/schema_test.py b/tests/schema_test.py index fba1359..2bff7ff 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -1,16 +1,19 @@ # Copyright (c) NiceBots.xyz # SPDX-License-Identifier: MIT -from glob import iglob -from os.path import splitext, basename import importlib +from glob import iglob +from os.path import basename, splitext from src.utils import validate_module -def test_ext_schemas(): +def test_ext_schemas() -> None: + """Test the schemas of all extensions.""" for ext in iglob("src/extensions/*"): name = splitext(basename(ext))[0] + if name.endswith(("_", "_/", ".py")): + continue module = importlib.import_module(f"src.extensions.{name}") validate_module(module) del module