From 253f7362f6b3a85165f59dc2f0b056fd0ac54b01 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Sat, 7 Sep 2024 15:12:46 -0400 Subject: [PATCH] Module-ify and add some type hinting; move to async sqlite3 lib (#572) * relocate all files into an actual overarching python module this facilitates packaging and relative imports also fixes some await issues and adds some type hints * rename main and add some hints/format strings * linting and hinting * aaaaa * lint * docs: reformat readme and update a few things * add script for running the bot and install uvloop in actual main function * fstrings * typing/linting * type hinting work * linting / code style / small fixes to strings * hinting * add aiosqlite and mypy typings for (optional) typechecking * relocate config & remove configurable schema location * oop * add new base cog which factors out some common functionality use it in basic cogs that dont need additional changes * relocate schema * add package manifest * docs: update info on config * rewrite bot.py with async database + some code style changes * rewrite main.py with aiosqlite and code style changes * rewrite banner cog with aiosqlite and code style changes * rewrite currency cog with aiosqlite and code style changes * rewrite customreactions cog with aiosqlite and refactoring * rewrite games cog with new base cog * rewrite helpers cog and images cog with new base cog * rewrite mod cog with aiosqlite and new base cog * change music cog to use new base cog * change quote cog to use aiosqlite and new base cog * change reminder cog to use new base cog * make add_member_if_needed util async * change score cog to new base cog + aiosqlite * comment * move Reminder.check_reminders to aiosqlite * convert mod cog to use aiosqlite * convert reminder cog to use aiosqlite * type hints for roles cog * remove unused import in score cog * cleanup in role check utils * add settings key del function in base cog * convert games cog to aiosqlite * convert helpers cog to aiosqlite * convert role restoration to aiosqlite * fix fetch_saved_roles not being async * respond with error msg if keydates encounters an http error * add error message if tex rendering fails * linting and autoformatting fixes * type fixes * lint * fix some typing complaints * fix another typing complaint * add bot health check function which checks if guild ID is set right * switch subscribers cog to new base cog fix guild errors switch to async fetch for metro status * change: switched to using lxml parser instead of python html.parser when invoking using bs4. (#573) * fix STM subscriber (?) by disabling content type checking in custom requests * add hashbang to hangman * mild refactor of customreactions cog * feat: higher level abstraction for fetching lists * lint * score code style * score reformatting * fix some funky strings * add a fetch_one higher level fn to base cog do a bit of reminder refactoring * clarifying parens * more reminders refactoring + lint * add (de)serialization to settings key methods; some related refactoring * f string * chore: more typing * chore: update dev dependencies & lockfile * lint * lint&hint * banner cog cleanup * more banner cleanup * currency cleanup * a bit of spacing * lint * update bot.py year * redo config class * type hint context in main.load fn * update dependencies, require poetry 1.4+ * update python & actions ver * new config integration work * single year range var for course url templates * new config: impl bet roll * ignore poetry toml * add example env file * fix supported python ver * ci: remove old ubuntus from matrix * chore [noci]: update copyright * chore: update more deps * chore: update pillow * lint * lint * fix: defaults for currency and music nested settings * fix: setting log levels with new config * fix: developer/moderator role checks * chore: port image config to new config object * fix: missing await for db fetchone * chore: port games config to new config model * fix: muted role checks * fix: music config model default start vol value * chore: port legacy assignable roles config to new config model * ci: tweak formatting check * fix: music config access * chore: update discordpy to 1.7.3 * fix: type hints causing bad conversions in currency cog * fix: type hints causing bad conversions in memes and roles cogs * chore: add log webhook env vars to example env * chore: remove old config class * fix: listing all roles not working * example env wording * [fix]: fixed field name in example.env * update course year range to 24-25 * steal https://github.com/idoneam/Canary/pull/519 * update dependencies * dockerfile work * some paginator type hinting * fix add_quote full name * fix some member converters in currency/quotes * custom reactions elifs * lint: run black * remove old config.ini * docs: update config / deployment details * ci: update actions * chore: clear apt lists + combine apt RUN statements * chore(deps): update uvloop * ci: build an edge (master-branch) image too --------- Co-authored-by: namemcguffin <52169277+namemcguffin@users.noreply.github.com> Co-authored-by: Mathieu B <47489943+le-potate@users.noreply.github.com> --- .dockerignore | 4 +- .github/workflows/formatting_check.yml | 9 +- .github/workflows/poetry-dev.yml | 15 +- .github/workflows/poetry-prod.yml | 10 +- .github/workflows/publish-docker.yml | 54 +- .gitignore | 1 + Dockerfile | 52 +- MANIFEST.in | 1 + README.md | 125 +- Martlet.schema => canary/Martlet.schema | 2 +- {cogs => canary}/__init__.py | 0 bot.py => canary/bot.py | 147 +- {config => canary/cogs}/__init__.py | 0 {cogs => canary/cogs}/banner.py | 396 +-- canary/cogs/base_cog.py | 100 + {cogs => canary/cogs}/currency.py | 325 ++- canary/cogs/customreactions.py | 960 ++++++++ {cogs => canary/cogs}/games.py | 84 +- {cogs => canary/cogs}/helpers.py | 346 +-- {cogs => canary/cogs}/images.py | 44 +- {cogs => canary/cogs}/info.py | 11 +- {cogs => canary/cogs}/memes.py | 61 +- {cogs => canary/cogs}/mod.py | 172 +- {cogs => canary/cogs}/music.py | 100 +- {cogs => canary/cogs}/quotes.py | 272 +-- {cogs => canary/cogs}/reminder.py | 321 ++- {cogs => canary/cogs}/roles.py | 58 +- {cogs => canary/cogs}/score.py | 249 +- {cogs => canary/cogs}/subscribers.py | 39 +- canary/cogs/utils/__init__.py | 0 {cogs => canary/cogs}/utils/arg_converter.py | 8 +- {cogs => canary/cogs}/utils/auto_incorrect.py | 14 +- {cogs => canary/cogs}/utils/checks.py | 21 +- {cogs => canary/cogs}/utils/clamp_default.py | 8 +- .../cogs}/utils/custom_requests.py | 3 +- {cogs => canary/cogs}/utils/dice_roll.py | 6 +- {cogs => canary/cogs}/utils/hangman.py | 25 +- {cogs => canary/cogs}/utils/image_helpers.py | 1 - {cogs => canary/cogs}/utils/members.py | 12 +- {cogs => canary/cogs}/utils/mock_context.py | 39 +- {cogs => canary/cogs}/utils/music_helpers.py | 28 +- {cogs => canary/cogs}/utils/p_strings.py | 16 +- {cogs => canary/cogs}/utils/paginator.py | 28 +- .../cogs}/utils/role_restoration.py | 122 +- {cogs => canary/cogs}/utils/site_save.py | 1 - {cogs => canary/cogs}/utils/subscribers.py | 0 canary/config/__init__.py | 5 + canary/config/config.py | 250 ++ Main.py => canary/main.py | 112 +- cogs/customreactions.py | 1235 ---------- cogs/utils/__init__.py | 18 - config/config.ini | 129 - config/parser.py | 176 -- example.env | 8 + poetry.lock | 2163 ++++++++++++----- pyproject.toml | 53 +- 56 files changed, 4657 insertions(+), 3782 deletions(-) create mode 100644 MANIFEST.in rename Martlet.schema => canary/Martlet.schema (98%) rename {cogs => canary}/__init__.py (100%) rename bot.py => canary/bot.py (50%) rename {config => canary/cogs}/__init__.py (100%) rename {cogs => canary/cogs}/banner.py (63%) create mode 100644 canary/cogs/base_cog.py rename {cogs => canary/cogs}/currency.py (52%) create mode 100644 canary/cogs/customreactions.py rename {cogs => canary/cogs}/games.py (86%) rename {cogs => canary/cogs}/helpers.py (77%) rename {cogs => canary/cogs}/images.py (71%) rename {cogs => canary/cogs}/info.py (88%) rename {cogs => canary/cogs}/memes.py (80%) rename {cogs => canary/cogs}/mod.py (83%) rename {cogs => canary/cogs}/music.py (90%) rename {cogs => canary/cogs}/quotes.py (63%) rename {cogs => canary/cogs}/reminder.py (56%) rename {cogs => canary/cogs}/roles.py (83%) rename {cogs => canary/cogs}/score.py (81%) rename {cogs => canary/cogs}/subscribers.py (86%) create mode 100644 canary/cogs/utils/__init__.py rename {cogs => canary/cogs}/utils/arg_converter.py (97%) rename {cogs => canary/cogs}/utils/auto_incorrect.py (89%) rename {cogs => canary/cogs}/utils/checks.py (67%) rename {cogs => canary/cogs}/utils/clamp_default.py (86%) rename {cogs => canary/cogs}/utils/custom_requests.py (90%) rename {cogs => canary/cogs}/utils/dice_roll.py (89%) rename {cogs => canary/cogs}/utils/hangman.py (90%) rename {cogs => canary/cogs}/utils/image_helpers.py (99%) rename {cogs => canary/cogs}/utils/members.py (73%) rename {cogs => canary/cogs}/utils/mock_context.py (54%) rename {cogs => canary/cogs}/utils/music_helpers.py (82%) rename {cogs => canary/cogs}/utils/p_strings.py (96%) rename {cogs => canary/cogs}/utils/paginator.py (95%) rename {cogs => canary/cogs}/utils/role_restoration.py (57%) rename {cogs => canary/cogs}/utils/site_save.py (96%) rename {cogs => canary/cogs}/utils/subscribers.py (100%) create mode 100644 canary/config/__init__.py create mode 100644 canary/config/config.py rename Main.py => canary/main.py (64%) delete mode 100644 cogs/customreactions.py delete mode 100644 cogs/utils/__init__.py delete mode 100644 config/config.ini delete mode 100644 config/parser.py create mode 100644 example.env diff --git a/.dockerignore b/.dockerignore index 71de48aef..57592148e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ -config/config.ini +canary/config/config.ini +data/runtime/*.db +data/runtime/*.obj env/ tests/ venv/ diff --git a/.github/workflows/formatting_check.yml b/.github/workflows/formatting_check.yml index 67f8e0d65..f28865409 100644 --- a/.github/workflows/formatting_check.yml +++ b/.github/workflows/formatting_check.yml @@ -3,15 +3,14 @@ name: formatting check on: push: paths: - "**.py" + - "**.py" jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 - uses: psf/black@stable with: - options: "--diff --check -t py310 --fast" - + options: "--diff --check -t py311 canary" diff --git a/.github/workflows/poetry-dev.yml b/.github/workflows/poetry-dev.yml index 540d528fb..3dbb01e3f 100644 --- a/.github/workflows/poetry-dev.yml +++ b/.github/workflows/poetry-dev.yml @@ -24,18 +24,16 @@ jobs: strategy: matrix: os: - - 'ubuntu-20.04' - - 'ubuntu-18.04' - # - 'macos-10.15' - - 'macos-11.0' - - 'windows-2019' + - 'ubuntu-24.04' + - 'macos-14.0' + - 'windows-2022' python-version: - - '3.10' + - '3.11' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -50,4 +48,3 @@ jobs: - name: install canary development dependencies run: poetry install --no-interaction - diff --git a/.github/workflows/poetry-prod.yml b/.github/workflows/poetry-prod.yml index 8cae68b13..b0adcfe97 100644 --- a/.github/workflows/poetry-prod.yml +++ b/.github/workflows/poetry-prod.yml @@ -17,15 +17,14 @@ jobs: strategy: matrix: os: - - 'ubuntu-20.04' - - 'ubuntu-18.04' + - 'ubuntu-24.04' python-version: - - '3.10' + - '3.11' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -40,4 +39,3 @@ jobs: - name: install canary production dependencies run: poetry install --no-dev --no-interaction - diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index d4b956d3c..a97a2c287 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -4,6 +4,10 @@ on: release: types: [published] + push: + branches: + - master + jobs: publish: runs-on: ubuntu-latest @@ -11,27 +15,53 @@ jobs: packages: write contents: read steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + + - name: Set up master (edge) image metadata + id: meta-master + uses: docker/metadata-action@v5 + if: ${{ github.event_name != 'release' }} with: - python-version: '3.10' - - run: pip install poetry && poetry update && poetry export -f requirements.txt > requirements.txt - - uses: docker/metadata-action@v3 - id: meta + images: | + ${{ inputs.image-name }} + flavor: | + latest=false + tags: | + type=raw,value=master,enable={{is_default_branch}} + type=sha,prefix=sha- + + - name: Set up release image metadata + id: meta-release + uses: docker/metadata-action@v5 + if: ${{ github.event_name == 'release' }} with: images: ghcr.io/idoneam/Canary + flavor: | + latest=true tags: | type=semver,pattern={{major}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{version}} - - uses: docker/setup-buildx-action@v1 - - uses: docker/login-action@v1 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 with: registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} - - uses: docker/build-push-action@v2 + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push master (edge) image + uses: docker/build-push-action@v6 + if: ${{ github.event_name != 'release' }} + with: + context: . + push: true + tags: ${{ steps.meta-master.outputs.tags }} + + - uses: docker/build-push-action@v6 + if: ${{ github.event_name == 'release' }} with: context: . push: true - tags: ${{ steps.meta.outputs.tags }} + tags: ${{ steps.meta-release.outputs.tags }} diff --git a/.gitignore b/.gitignore index 7538d8ee0..d0b3cff07 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,7 @@ ENV/ # configuration files **/*.ini +poetry.toml # other *~ diff --git a/Dockerfile b/Dockerfile index 8c647aa06..43e110ff2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,38 @@ -FROM python:3.10-slim-bullseye +FROM python:3.11-slim-bookworm -# Install base apt dependencies -RUN apt-get update && apt-get install -y git sqlite3 +# Install base apt dependencies + auxiliary dependencies (for GL, Tex, etc.) +RUN apt-get update && \ + apt-get install -y \ + git \ + sqlite3 \ + libgl1-mesa-glx \ + texlive-latex-extra \ + texlive-fonts-extra \ + texlive-lang-greek \ + dvipng \ + ffmpeg \ + gcc && \ + rm -rf /var/lib/apt/lists/* -# Install auxiliary dependencies (for GL, Tex, etc.) -RUN apt-get install -y \ - libgl1-mesa-glx \ - texlive-latex-extra \ - texlive-fonts-extra \ - texlive-lang-greek \ - dvipng \ - ffmpeg \ - gcc +# Update pip, install poetry +RUN pip install --no-cache-dir -U pip; \ + pip install --no-cache-dir poetry==1.8.3 -# Install requirements with pip to use Docker cache independent of project metadata -COPY requirements.txt / -RUN pip install -r /requirements.txt +COPY pyproject.toml . +COPY poetry.lock . +RUN poetry config virtualenvs.create false && \ + poetry --no-cache install --no-root --without dev -# Copy code to the `canary` directory in the image and run the bot from there -COPY . /canary -WORKDIR /canary +# Copy code and pre-made data to the /app directory, where the bot will be run from +WORKDIR /app +COPY canary canary +COPY data data + +RUN pip install -e . # Notes: -# Users will have to mount their config.ini in by hand -# Users should mount a read/writable volume for /canary/data/runtime +# Users will have to configure their instance using environment variables, described by the Pydantic settings object +# in /app/canary/config/config.py +# Users should mount a read/writable volume for /app/data/runtime -CMD ["python3.10", "Main.py"] +CMD ["canary"] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..bae6f261e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include canary/Martlet.schema diff --git a/README.md b/README.md index 7f566a94b..a7e62c9f5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![Discord](https://img.shields.io/discord/236668784948019202.svg)](https://discord.gg/HDHvv58) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -Canary is a Python3 bot designed for the McGill University Community Discord Server. The bot provides helper functions to users, as well as fun functions, a quote database and custom greeting messages. +Canary is a Python3 bot designed for the McGill University Community Discord Server. The bot provides helper functions +to users, as well as fun functions, a quote database and custom greeting messages. ## Build Statuses @@ -11,43 +12,53 @@ Canary is a Python3 bot designed for the McGill University Community Discord Ser ## Installation -1. If you wish to use the `update` command to update to the latest version of the bot, configure your github account in -your environment of choice and clone into the repository with: +1. If you wish to use the `update` command to update to the latest version of the bot, configure your GitHub account in + your environment of choice and clone into the repository with: -```bash -git clone https://github.com/idoneam/Canary -``` + ```bash + git clone https://github.com/idoneam/Canary + ``` -2. Dependencies are managed with poetry which can be installed via pip with: +2. Dependencies are managed with `poetry` (1.8.3+), which can be installed via `pip` with: -```bash -python3 -m pip install poetry -``` + ```bash + python3 -m pip install poetry + ``` 3. Dependencies may be installed using poetry with the following command: -```bash -poetry install --no-dev -``` + ```bash + poetry install --no-dev + ``` -4. Use of the LaTeX equation functionality requires a working LaTeX installation (at the very minimum, `latex` and `dvipng` must be present). The easiest way to do this is to install TeX Live (usually possible through your distro's package manager, or through TeX Live's own facilities for the latest version). See the [TeX Live site](https://tug.org/texlive/) for more information. +4. Use of the LaTeX equation functionality requires a working LaTeX installation (at the very minimum, `latex` and + `dvipng` must be present). The easiest way to do this is to install TeX Live (usually possible through your distro's + package manager, or through TeX Live's own facilities for the latest version). See the + [TeX Live site](https://tug.org/texlive/) for more information. 5. Development dependencies (YAPF and `pytest`) can be installed alongside all other dependencies with: -```bash -poetry install -``` + ```bash + poetry install + ``` -6. You may enter the virtual environment generated by the pipenv installation with: `$ poetry shell` or simply run the bot with `$ poetry run python3 Main.py` +6. You may enter the virtual environment generated by the Poetry installation with: `poetry shell` or simply run the + bot with `poetry run canary` -7. In order to run bots on Discord, you need to [create a bot account](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token). +7. In order to run bots on Discord, you need to + [create a bot account](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token). -8. In the Discord Developer Portal, you must enable the "Presence" and "Server Members" Privileged Gateway Intents (In the Bot tab of your application) +8. In the Discord Developer Portal, you must enable the "Presence" and "Server Members" Privileged Gateway Intents (In + the Bot tab of your application) + +You must set certain values in the `config.ini` file, in particular your Discord bot token (which you get in the +previous link) and the values in the `[Server]` section. -You must set certain values in the `config.ini` file, in particular your Discord bot token (which you get in the previous link) and the values in the `[Server]` section.
Click here to see descriptions for a few of those values

-(For values that use Discord IDs, see [this](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) to know how to find them) +(For values that use Discord IDs, see +[this](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) to know +how to find them) * `[Discord]` * `Key`: Your Discord bot token. @@ -61,21 +72,26 @@ You must set certain values in the `config.ini` file, in particular your Discor * `BannerVoteEmoji`: The name of the emoji that is used to vote on Banner of the Week Contests. * `[Roles]` * `ModeratorRole`: The name of the role that your moderators have (for functions like DMing users). - * `DeveloperRole`: The name of the role that your developers have (for functions like restarting the bot). This could be the same role than moderator. + * `DeveloperRole`: The name of the role that your developers have (for functions like restarting the bot). This + could be the same role as moderator. * `McgillianRole`: The name of the role that verified McGillians have. * `HonoraryMcGillianRole`: The name of the role that Honorary McGillians (verified Non-McGillians) have. * `BannerRemindersRole`: The name of the role that is pinged when a Banner of the Week Contest starts. * `BannerWinnerRole`: The name of the role that is given to users that win a Banner of the Week Contest. * `TrashTierBannerRole`: The name of the role that is given to users that are banned from submitting in Banner of the Week Contests. * `NoFoodSpottingRole`: The name of the role assigned to abusers of the foodspotting command that will prevent them from using it. - * `MutedRole`: **(Optional)** The name of the role used to mute users and create an appeal channel for them. (The role presumably - also removes some permissions; exact role implementation is up to the server's administrators.) + * `MutedRole`: **(Optional)** The name of the role used to mute users and create an appeal channel for them. (The + role presumably also removes some permissions; exact role implementation is up to the server's administrators.) * `SecretCrabbo`: The name of the role for users that wish to be pinged for secret crabbo celebrations. * `[Channels]` - * `ReceptionChannel`: The name of the channel that will receive messages sent to the bot through the `answer` command (and where messages sent by mods to users with the `dm` command will be logged) - * `BannerOfTheWeekChannel`: The name of the channel where winning submissions for Banner of the Week Contests are sent. - * `BannerSubmissionsChannel`: The name of the channel where submissions for Banner of the Week Contests are sent. This is where users vote. - * `BannerConvertedChannel`: The name of the channel where the converted submissions for Banner of the Week Contests are sent. This is where the bot will fetch the winning banner. + * `ReceptionChannel`: The name of the channel that will receive messages sent to the bot through the `answer` + command (and where messages sent by mods to users with the `dm` command will be logged) + * `BannerOfTheWeekChannel`: The name of the channel where winning submissions for Banner of the Week Contests are + sent. + * `BannerSubmissionsChannel`: The name of the channel where submissions for Banner of the Week Contests are sent. + This is where users vote. + * `BannerConvertedChannel`: The name of the channel where the converted submissions for Banner of the Week Contests + are sent. This is where the bot will fetch the winning banner. * `FoodSpottingChannel`: The name of the channel where foodspotting posts are sent. * `MetroStatusChannel`: The name of the channel where metro status alerts are sent. * `BotsChannel`: The name of the channel for bot spamming. @@ -85,14 +101,22 @@ You must set certain values in the `config.ini` file, in particular your Discor * `[Meta]` * `Repository`: The HTTPS remote for this repository, used by the `update` command as the remote when pulling. * `[Logging]` - * `LogLevel`: [See this for a list of levels](https://docs.python.org/3/library/logging.html#levels). Logs from exceptions and commands like `mix` and `bac` are at the `info` level. Logging messages from the level selected *and* from more severe levels will be sent to your logging file. For example, setting the level to `info` also sends logs from `warning`, `error` and `critical`, but not from `debug`. - * `LogFile`: The file where the logging output will be sent (will be created there by the bot if it doesn't exist). Note that all logs are sent there, including those destined for devs and those destined for mods. - * `DevLogWebhookID`: Optional. If the ID of a webhook is input (and it's token below), logs destined for devs will also be sent to it. These values are contained in the discord webhook url: [discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN](discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN) + * `LogLevel`: [See this for a list of levels](https://docs.python.org/3/library/logging.html#levels). Logs from + exceptions and commands like `mix` and `bac` are at the `info` level. Logging messages from the level selected + *and* from more severe levels will be sent to your logging file. For example, setting the level to `info` also + sends logs from `warning`, `error` and `critical`, but not from `debug`. + * `LogFile`: The file where the logging output will be sent (will be created there by the bot if it doesn't exist). + Note that all logs are sent there, including those destined for devs and those destined for mods. + * `DevLogWebhookID`: Optional. If the ID of a webhook is input (and it's token below), logs destined for devs will + also be sent to it. These values are contained in the discord webhook url: + `discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN` * `DevLogWebhookToken`: Optional. See above. - * `ModLogWebhookID`: Optional. If the ID of a webhook is input (and it's token below), logs destined for mods will also be sent to it. See the URL above to see how to find those values. + * `ModLogWebhookID`: Optional. If the ID of a webhook is input (and it's token below), logs destined for mods will + also be sent to it. See the URL above to see how to find those values. * `ModLogWebhookToken`: Optional. See above. * `[DB]` - * `Schema`: Location of the Schema file that creates tables in the database (This file already exists so you shouldn't have to change this unless you rename it or change its location). + * `Schema`: Location of the Schema file that creates tables in the database (This file already exists, so you + shouldn't have to change this unless you rename it or change its location). * `Path`: Your database file path (will be created there by the bot if it doesn't exist). * `[Helpers]` * `CourseTemplate`: McGill course schedule URL. **Changes every school year.** @@ -105,8 +129,10 @@ You must set certain values in the `config.ini` file, in particular your Discor * `FoodRecallChannel`: Channel where you want CFIA recall notices posted. * `FoodRecallLocationFilter`: Regions you want to receive CFIA recall notices for. * `FoodSpottingChannel`: Channel where you want foodspotting posts to be sent, ideally in a dedicated channel. - * `NoFoodSpottingRole`: Name of role assigned to abusers of the foodspotting command that will prevent them from using it. - * `MetroStatusChannel`: Channel where you want metro status alerts to be sent, ideally in a dedicated channel with opt-in read permissions for users. + * `NoFoodSpottingRole`: Name of role assigned to abusers of the foodspotting command that will prevent them from using + it. + * `MetroStatusChannel`: Channel where you want metro status alerts to be sent, ideally in a dedicated channel with + opt-in read permissions for users. * `[Currency]` * `Name`: The name of the bot currency. * `Symbol`: The currency's symbol (e.g. `$`). @@ -147,7 +173,8 @@ If you installed all dev dependencies, you can run tests with `poetry run pytest ## Running the bot -Run `poetry run python Main.py` in your shell. Ensure that your Discord token is set in the `config.ini` file within the `config` directory. +Run `poetry run canary` in your shell. Ensure that your Discord token is set in the `config.ini` file within the +`canary/config` directory. ### Docker Container @@ -169,8 +196,12 @@ docker build -t canary:latest . #### Running the Container -You will need to download and modify Canary's [config.ini](config/config.ini) to your -liking, so it can be mounted inside the container. +You will need to set environment variables according to the values configured in the Pydantic +configuration object described in [config.py](canary/config/config.py). These environment variables +correspond to the properties of the object, uppercased, underscore-spaced, and prefixed with `CANARY_.` + +A bare minimum set of environment variables to set can be found in [example.env](./example.env). +These variables must be set inside the container in order for the bot to function. From within the root of the repository: @@ -180,8 +211,8 @@ mkdir -f runtime-data # Run the container docker run -d \ - -v $(pwd)/config.ini:/canary/config/config.ini:ro \ - -v $(pwd)/runtime-data:/canary/data/runtime \ + --env-file my.env \ + -v $(pwd)/runtime-data:/app/data/runtime \ canary:latest ``` @@ -189,18 +220,22 @@ Optionally provide the `-d` flag to run the container in detached state. ## Code Linting -We format our code using PSF's [black](https://github.com/psf/black). Our builds will reject code that do not conform to the standards defined in [`pyproject.toml`](https://black.readthedocs.io/en/stable/pyproject_toml.html) . You may format your code using: +We format our code using PSF's [black](https://github.com/psf/black). Our builds will reject code that do not conform to +the standards defined in [`pyproject.toml`](https://black.readthedocs.io/en/stable/pyproject_toml.html) . You may format +your code using: ``` -poetry run black . -t py310 --fast +poetry run black . -t py310 ``` and ensure your code conforms to our linting with : ``` -poetry run black --diff . -t py310 --fast +poetry run black --diff . -t py310 ``` ## Contributions -Contributions are welcome, feel free to fork our repository and open a pull request or open an issue. Please [review our contribution guidelines](https://github.com/idoneam/Canary/blob/dev/.github/contributing.md) before contributing. +Contributions are welcome, feel free to fork our repository and open a pull request or open an issue. +Please [review our contribution guidelines](https://github.com/idoneam/Canary/blob/dev/.github/contributing.md) +before contributing. diff --git a/Martlet.schema b/canary/Martlet.schema similarity index 98% rename from Martlet.schema rename to canary/Martlet.schema index 96c146a67..0f0786fda 100644 --- a/Martlet.schema +++ b/canary/Martlet.schema @@ -1,5 +1,5 @@ /* - * Copyright (C) idoneam (2016-2022) + * Copyright (C) idoneam (2016-2023) * * This file is part of Canary * diff --git a/cogs/__init__.py b/canary/__init__.py similarity index 100% rename from cogs/__init__.py rename to canary/__init__.py diff --git a/bot.py b/canary/bot.py similarity index 50% rename from bot.py rename to canary/bot.py index 535f089e7..9a0a028fd 100644 --- a/bot.py +++ b/canary/bot.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -18,48 +15,60 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -from discord.ext import commands - -from config import parser +import aiosqlite +import contextlib import logging -import sqlite3 import traceback -import requests -from discord import Webhook, RequestsWebhookAdapter, Intents - -__all__ = ["bot", "developer_role", "moderator_role", "muted_role"] -_parser = parser.Parser() -command_prefix = _parser.command_prefix +from canary.config import Config +from discord import Webhook, RequestsWebhookAdapter, Intents +from discord.ext import commands +from pathlib import Path +from typing import AsyncGenerator + +__all__ = [ + "config", # Some functions/mixins use global config instead of some form of DI + "Canary", + "bot", +] + +LOG_LEVELS = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, + "notset": logging.NOTSET, +} + +config: Config = Config() +log_level = LOG_LEVELS[config.log_level] # Create parent logger, which will send all logs from the "sub-loggers" # to the specified log file -_logger = logging.getLogger("Canary") -_logger.setLevel(_parser.log_level) -_file_handler = logging.FileHandler(filename=_parser.log_file, encoding="utf-8", mode="a") -_file_handler.setFormatter(logging.Formatter("[%(levelname)s] %(asctime)s: %(message)s")) -_logger.addHandler(_file_handler) +logger = logging.getLogger("Canary") +logger.setLevel(log_level) +file_handler = logging.FileHandler(filename=config.log_file, encoding="utf-8", mode="a") +file_handler.setFormatter(logging.Formatter("[%(levelname)s] %(asctime)s: %(message)s")) +logger.addHandler(file_handler) # Create dev (sub-)logger, which is where errors and messages are logged # If a dev webhook is specified, logs sent to the dev logger will be # sent to the webhook -_dev_logger = logging.getLogger("Canary.Dev") -_dev_logger.setLevel(_parser.log_level) +dev_logger = logging.getLogger("Canary.Dev") +dev_logger.setLevel(log_level) # Create mod (sub-)logger, where info for mods will be logged # If a mod webhook is specified, logs sent to the mod logger will be # sent to the webhook. This is always set to the INFO level, since this is # where info for mods is logged -_mod_logger = logging.getLogger("Canary.Mod") -_mod_logger.setLevel(logging.INFO) +mod_logger = logging.getLogger("Canary.Mod") +mod_logger.setLevel(logging.INFO) class _WebhookHandler(logging.Handler): def __init__(self, webhook_id, webhook_token, username=None): - if not username: - self.username = "Bot Logs" - else: - self.username = username + self.username = username or "Bot Logs" logging.Handler.__init__(self) self.webhook = Webhook.partial(webhook_id, webhook_token, adapter=RequestsWebhookAdapter()) @@ -68,46 +77,68 @@ def emit(self, record): self.webhook.send(f"```\n{msg}```", username=self.username) -if _parser.dev_log_webhook_id and _parser.dev_log_webhook_token: - _dev_webhook_username = f"{_parser.bot_name} Dev Logs" - _dev_webhook_handler = _WebhookHandler( - _parser.dev_log_webhook_id, _parser.dev_log_webhook_token, username=_dev_webhook_username +if config.dev_log_webhook_id and config.dev_log_webhook_token: + dev_webhook_username = f"{config.bot_name} Dev Logs" + dev_webhook_handler = _WebhookHandler( + config.dev_log_webhook_id, config.dev_log_webhook_token, username=dev_webhook_username ) - _dev_webhook_handler.setFormatter(logging.Formatter("[%(levelname)s] %(asctime)s:\n%(message)s")) - _dev_logger.addHandler(_dev_webhook_handler) + dev_webhook_handler.setFormatter(logging.Formatter("[%(levelname)s] %(asctime)s:\n%(message)s")) + dev_logger.addHandler(dev_webhook_handler) -if _parser.mod_log_webhook_id and _parser.mod_log_webhook_token: - _mod_webhook_username = f"{_parser.bot_name} Mod Logs" - _mod_webhook_handler = _WebhookHandler( - _parser.mod_log_webhook_id, _parser.mod_log_webhook_token, username=_mod_webhook_username +if config.mod_log_webhook_id and config.mod_log_webhook_token: + mod_webhook_username = f"{config.bot_name} Mod Logs" + mod_webhook_handler = _WebhookHandler( + config.mod_log_webhook_id, config.mod_log_webhook_token, username=mod_webhook_username ) - _mod_webhook_handler.setFormatter(logging.Formatter("[%(levelname)s] %(asctime)s:\n%(message)s")) - _mod_logger.addHandler(_mod_webhook_handler) + mod_webhook_handler.setFormatter(logging.Formatter("[%(levelname)s] %(asctime)s:\n%(message)s")) + mod_logger.addHandler(mod_webhook_handler) class Canary(commands.Bot): + SCHEMA_PATH = Path(__file__).parent / "Martlet.schema" + def __init__(self, *args, **kwargs): - super().__init__(command_prefix, *args, **kwargs) - self.logger = _logger - self.dev_logger = _dev_logger - self.mod_logger = _mod_logger - self.config = _parser - self._start_database() - - def _start_database(self): + super().__init__(config.command_prefix, *args, **kwargs) + self.logger = logger + self.dev_logger = dev_logger + self.mod_logger = mod_logger + self.config = config + + async def start(self, *args, **kwargs): # TODO: discordpy 2.0: use setup_hook for database setup + await self._start_database() + await super().start(*args, **kwargs) + await self.health_check() + + @contextlib.asynccontextmanager + async def db(self) -> AsyncGenerator[aiosqlite.Connection, None]: + conn: aiosqlite.Connection + async with aiosqlite.connect(self.config.db_path) as conn: + await conn.execute("PRAGMA foreign_keys = ON") + yield conn + + async def db_nocm(self) -> aiosqlite.Connection: + return await aiosqlite.connect(self.config.db_path) + + async def _start_database(self): if not self.config.db_path: self.dev_logger.warning("No path to database configuration file") return self.dev_logger.debug("Initializing SQLite database") - conn = sqlite3.connect(self.config.db_path) - c = conn.cursor() - with open(self.config.db_schema_path) as fp: - c.executescript(fp.read()) - conn.commit() - conn.close() + + db: aiosqlite.Connection + async with self.db() as db: + with open(Canary.SCHEMA_PATH) as fp: + await db.executescript(fp.read()) + await db.commit() + self.dev_logger.debug("Database is ready") + async def health_check(self): + guild = self.get_guild(self.config.server_id) + if not guild: + self.dev_logger.error(f"Could not get guild for bot (specified server ID {self.config.server_id})") + def log_traceback(self, exception): self.dev_logger.error("".join(traceback.format_exception(type(exception), exception, exception.__traceback__))) @@ -126,11 +157,11 @@ async def on_command_error(self, ctx, error): return elif isinstance(error, commands.DisabledCommand): - return await ctx.send("{} has been disabled.".format(ctx.command)) + return await ctx.send(f"{ctx.command} has been disabled.") elif isinstance(error, commands.NoPrivateMessage): try: - return await ctx.author.send("{} can not be used in Private Messages.".format(ctx.command)) + return await ctx.author.send(f"{ctx.command} can not be used in Private Messages.") except Exception: pass @@ -146,15 +177,11 @@ async def on_command_error(self, ctx, error): f"per {error.per.name}" ) - self.dev_logger.error("Ignoring exception in command {}:".format(ctx.command)) + self.dev_logger.error(f"Ignoring exception in command {ctx.command}:") self.log_traceback(error) -# predefined variables to be imported intents = Intents.default() intents.members = True intents.presences = True bot = Canary(case_insensitive=True, intents=intents) -moderator_role = bot.config.moderator_role -developer_role = bot.config.developer_role -muted_role = bot.config.muted_role diff --git a/config/__init__.py b/canary/cogs/__init__.py similarity index 100% rename from config/__init__.py rename to canary/cogs/__init__.py diff --git a/cogs/banner.py b/canary/cogs/banner.py similarity index 63% rename from cogs/banner.py rename to canary/cogs/banner.py index d1bbf0009..3d5d2098c 100644 --- a/cogs/banner.py +++ b/canary/cogs/banner.py @@ -14,42 +14,56 @@ # # You should have received a copy of the GNU General Public License # along with Canary. If not, see . - # discord-py requirements import discord from discord.ext import commands, tasks from discord import utils # Other utilities -from io import BytesIO +import asyncio import datetime -import sqlite3 +import json import requests +from io import BytesIO from PIL import Image, UnidentifiedImageError, ImageSequence -import json + +from ..bot import Canary +from .base_cog import CanaryCog from .utils.checks import is_moderator -import asyncio -class Banner(commands.Cog): +class Banner(CanaryCog): + CONVERTER_FILE = "./data/premade/banner_converter.png" + PREVIEW_FILE = "./data/premade/banner_preview.png" + + PREVIEW_MASK_USER_CANVAS_SIZE = (240, 135) + PREVIEW_MASK_USER_BOX_START = (5, 5) + TRANSPARENT = (0, 0, 0, 0) + # Written by @le-potate - def __init__(self, bot): - self.bot = bot - self.guild = None - self.banner_reminders_role = None - self.banner_of_the_week_channel = None - self.banner_submissions_channel = None - self.banner_converted_channel = None - self.bots_channel = None - self.banner_winner_role = None - self.banner_vote_emoji = None - self.start_datetime = None - self.week_name = None - self.send_reminder = None - - @commands.Cog.listener() + def __init__(self, bot: Canary): + super().__init__(bot) + + self.banner_of_the_week_channel: discord.TextChannel | None = None + self.banner_submissions_channel: discord.TextChannel | None = None + self.banner_converted_channel: discord.TextChannel | None = None + self.bots_channel: discord.TextChannel | None = None + + self.banner_reminders_role: discord.Role | None = None + self.banner_winner_role: discord.Role | None = None + self.banner_vote_emoji: discord.Emoji | None = None + + self.start_datetime: datetime.datetime | None = None + self.week_name: str | None = None + self.send_reminder: str | None = None + + @CanaryCog.listener() async def on_ready(self): - self.guild = self.bot.get_guild(self.bot.config.server_id) + await super().on_ready() + + if not self.guild: + return + self.banner_of_the_week_channel = utils.get( self.guild.text_channels, name=self.bot.config.banner_of_the_week_channel ) @@ -64,21 +78,25 @@ async def on_ready(self): self.banner_winner_role = utils.get(self.guild.roles, name=self.bot.config.banner_winner_role) self.banner_vote_emoji = utils.get(self.guild.emojis, name=self.bot.config.banner_vote_emoji) - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("BannerContestInfo",)) - fetched = c.fetchone() - if fetched: - banner_dict = json.loads(fetched[0]) - timestamp = banner_dict["timestamp"] - if timestamp: + banner_dict: dict | None + if (banner_dict := await self.get_banner_contest_info()) is not None: + if timestamp := banner_dict["timestamp"]: self.start_datetime = datetime.datetime.fromtimestamp(timestamp) self.week_name = banner_dict["week_name"] self.send_reminder = banner_dict["send_reminder"] - conn.close() self.check_banner_contest_reminder.start() + async def get_banner_contest_info(self): + return await self.get_settings_key("BannerContestInfo", deserialize=json.loads) + + async def set_banner_contest_info(self, banner_dict: dict, pre_commit: list[str] | None = None): + timestamp = banner_dict.get("timestamp") + self.start_datetime = datetime.datetime.fromtimestamp(timestamp) if timestamp is not None else None + self.week_name = banner_dict.get("week_name") + self.send_reminder = banner_dict.get("send_reminder") + await self.set_settings_key("BannerContestInfo", banner_dict, serialize=json.dumps, pre_commit=pre_commit) + @tasks.loop(minutes=1.0) async def check_banner_contest_reminder(self): # todo: make general scheduled events db instead @@ -88,8 +106,6 @@ async def check_banner_contest_reminder(self): if datetime.datetime.now() < self.start_datetime or not self.send_reminder: return - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() await self.banner_submissions_channel.send( f"{self.banner_reminders_role.mention} " f"Submissions are now open for the banner picture of the week! " @@ -97,28 +113,13 @@ async def check_banner_contest_reminder(self): f"The winner will be chosen in around 12 hours " f"(To get these reminders, type `.iam Banner Submissions` in {self.bots_channel.mention})" ) - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("BannerContestInfo",)) - fetched = c.fetchone() - if fetched: - self.send_reminder = False - banner_dict = json.loads(fetched[0]) - banner_dict["send_reminder"] = False - c.execute("REPLACE INTO Settings VALUES (?, ?)", ("BannerContestInfo", json.dumps(banner_dict))) - conn.commit() - conn.close() + + banner_dict: dict | None + if (banner_dict := await self.get_banner_contest_info()) is not None: + await self.set_banner_contest_info({**banner_dict, "send_reminder": False}) async def reset_banner_contest(self): - self.start_datetime = None - self.week_name = None - self.send_reminder = None - - banner_dict = {"timestamp": None, "week_name": None, "send_reminder": None} - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("REPLACE INTO Settings VALUES (?, ?)", ("BannerContestInfo", json.dumps(banner_dict))) - c.execute("DELETE FROM BannerSubmissions") - conn.commit() - conn.close() + await self.set_banner_contest_info({"timestamp": None, "week_name": None, "send_reminder": None}) @commands.command(aliases=["setbannercontest"]) @is_moderator() @@ -133,11 +134,13 @@ async def set_banner_contest(self, ctx): You must be a moderator to use this command. """ + if "BANNER" not in self.guild.features: await ctx.send( "Warning: This server cannot currently upload and use a banner. " "You may still set the next banner contest." ) + if self.start_datetime: if datetime.datetime.now() < self.start_datetime: await ctx.send( @@ -165,12 +168,14 @@ def msg_check(msg): except asyncio.TimeoutError: await ctx.send("Command timed out.") return - date_str = date_msg.content - if date_str.lower() == "quit": + date_str = date_msg.content.lower() + + if date_str == "quit": await ctx.send("Command exited.") return - elif date_str.lower() == "now": + + if date_str == "now": timestamp = datetime.datetime.now().timestamp() else: try: @@ -189,34 +194,28 @@ def msg_check(msg): except asyncio.TimeoutError: await ctx.send("Command timed out.") return - week_name = week_msg.content - banner_dict = {"timestamp": timestamp, "week_name": week_name, "send_reminder": True} - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("REPLACE INTO Settings VALUES (?, ?)", ("BannerContestInfo", json.dumps(banner_dict))) - c.execute("DELETE FROM BannerSubmissions") - conn.commit() + week_name = week_msg.content - self.start_datetime = datetime.datetime.fromtimestamp(timestamp) - self.week_name = week_name - self.send_reminder = True + await self.set_banner_contest_info( + {"timestamp": timestamp, "week_name": week_name, "send_reminder": True}, + pre_commit=["DELETE FROM BannerSubmissions"], + ) await ctx.send( f"Start time for the banner contest of the week of `{week_name}` successfully set to " f"`{self.start_datetime.strftime('%Y-%m-%d %H:%M')}`." ) - conn.close() @commands.command(aliases=["bannerwinner", "setbannerwinner", "set_banner_winner"]) @is_moderator() - async def banner_winner(self, ctx, winner: discord.Member = None): + async def banner_winner(self, ctx: commands.Context, winner: discord.Member = None): """ Select the winner for an ongoing Banner Picture of the Week contest - The winning picture is then set as the server's Banner and the submission is published on the Banner of the week channel. - The winning user receives the Banner of the Week Winner role, and the submission previews are pinned in the - Banner Submissions and Converted Banner Submissions channels. + The winning picture is then set as the server's Banner and the submission is published on the Banner of the week + channel. The winning user receives the Banner of the Week Winner role, and the submission previews are pinned in + the Banner Submissions and Converted Banner Submissions channels. This command can be used with a user as argument. Otherwise, a prompt will ask for the user. The user must have submitted a banner using the submitbanner command during the contest. @@ -224,6 +223,22 @@ async def banner_winner(self, ctx, winner: discord.Member = None): You must be a moderator to use this command. """ + + if not self.guild: + return + + if not self.banner_of_the_week_channel: + await ctx.send("No banner of the week channel set.") + return + + if not self.banner_submissions_channel: + await ctx.send("No banner submissions channel set.") + return + + if not self.banner_converted_channel: + await ctx.send("No converted banner channel set.") + return + if not self.start_datetime: await ctx.send("There is no banner contest right now.") return @@ -239,8 +254,7 @@ def msg_check(msg): if not winner: await ctx.send( - "Please enter the username of the winner " - "or `None` to end the contest without any winner.\n" + "Please enter the username of the winner or `None` to end the contest without any winner.\n" "Type `quit` to leave." ) @@ -251,30 +265,28 @@ def msg_check(msg): return winner_str = winner_msg.content - if winner_str.lower() == "none": - await self.reset_banner_contest() - await ctx.send("Successfully ended banner contest.") - return - elif winner_str.lower() == "quit": - await ctx.send("Command exited.") - return - else: - try: - winner = await commands.MemberConverter().convert(ctx, winner_str) - except commands.BadArgument: - await ctx.send("Could not find user.") + match winner_str.lower(): + case "none": + await self.reset_banner_contest() + await ctx.send("Successfully ended banner contest.") + return + case "quit": + await ctx.send("Command exited.") return + case _: + try: + winner = await commands.MemberConverter().convert(ctx, winner_str) + except commands.BadArgument: + await ctx.send("Could not find user.") + return if "BANNER" not in self.guild.features: await ctx.send("This server cannot upload and use a banner") return winner_id = winner.id - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT * FROM BannerSubmissions WHERE UserID = ?", (winner_id,)) - fetched = c.fetchone() - conn.close() + + fetched = await self.fetch_one("SELECT * FROM BannerSubmissions WHERE UserID = ?", (winner_id,)) if not fetched: await ctx.send("No submission by this user in database. Exiting command.") @@ -291,6 +303,7 @@ def msg_check(msg): f"It might have been manually deleted. Exiting command." ) return + try: converted_message = await self.banner_converted_channel.fetch_message(converted_message_id) except discord.errors.NotFound: @@ -328,8 +341,7 @@ def msg_check(msg): await ctx.send("Command timed out.") return - confirmation_str = confirmation_msg.content - if confirmation_str.lower() != "yes": + if confirmation_msg.content.lower() != "yes": await ctx.send("Exiting without selecting winner.") return @@ -344,56 +356,63 @@ def msg_check(msg): try: await preview_message.pin( - reason=f"Banner of the week winner submitted by {winner} " f"(Approved by {ctx.author})" + reason=f"Banner of the week winner submitted by {winner} (Approved by {ctx.author})" ) except discord.errors.HTTPException as e: - if e.code == 30003: # Discord API code for full pins - pins = await self.banner_submissions_channel.pins() - await pins[-1].unpin(reason="#banner_submissions pins are full") - await preview_message.pin( - reason=f"Banner of the week winner submitted by {winner} " f"(Approved by {ctx.author})" - ) - else: + if e.code != 30003: # Discord API code for full pins raise e + pins = await self.banner_submissions_channel.pins() + await pins[-1].unpin(reason="#banner_submissions pins are full") + await preview_message.pin( + reason=f"Banner of the week winner submitted by {winner} (Approved by {ctx.author})" + ) try: await converted_message.pin( - reason=f"Banner of the week winner submitted by {winner} " f"(Approved by {ctx.author})" + reason=f"Banner of the week winner submitted by {winner} (Approved by {ctx.author})" ) except discord.errors.HTTPException as e: - if e.code == 30003: - pins = await self.banner_converted_channel.pins() - await pins[-1].unpin(reason="#converted_banner_submissions pins are full") - await converted_message.pin( - reason=f"Banner of the week winner submitted by {winner} " f"(Approved by {ctx.author})" - ) - else: + if e.code != 30003: raise e + pins = await self.banner_converted_channel.pins() + await pins[-1].unpin(reason="#converted_banner_submissions pins are full") + await converted_message.pin( + reason=f"Banner of the week winner submitted by {winner} (Approved by {ctx.author})" + ) await winner.add_roles(self.banner_winner_role, reason=f"Banner of the week winner (Approved by {ctx.author})") converted_read = await converted.read() await self.guild.edit( banner=converted_read, - reason=f"Banner of the week winner submitted by {winner} " f"(Approved by {ctx.author})", + reason=f"Banner of the week winner submitted by {winner} (Approved by {ctx.author})", ) await self.reset_banner_contest() await ctx.send("Successfully set banner and ended contest.") + @staticmethod + def calc_ratio_max(canvas_dims: tuple[int, int], frame: Image) -> float: + return max(canvas_dims[0] / frame.size[0], canvas_dims[1] / frame.size[1]) + @commands.command(aliases=["submitbanner"]) - async def submit_banner(self, ctx, *args): + async def submit_banner(self, ctx: commands.Context, *args): """ - Submit a picture for an Banner Picture of the Week contest + Submit a picture for a Banner Picture of the Week contest - There must be an ongoing Banner contest to use this command; check the Banner of the Week channel for more information. - This command can be used in a picture caption or with a url as argument. - The image will be scaled to maximum fit and centered; you can add -stretch to the command for the image to be stretched instead. + There must be an ongoing Banner contest to use this command; check the Banner of the Week channel for more + information. + This command can be used in a picture caption or with a URL as an argument. + The image will be scaled to maximum fit and centered; you can add -stretch to the command for the image to be + stretched instead. For better results, your picture must be at least 960x540 pixels in a 16:9 aspect ratio. You must be a verified user to use this command. """ - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + if not self.banner_submissions_channel: + await ctx.send("No banner submissions channel set.") + return + + # TODO: this is not portable to other server setups if not ( discord.utils.get(ctx.author.roles, name=self.bot.config.mcgillian_role) or discord.utils.get(ctx.author.roles, name=self.bot.config.honorary_mcgillian_role) @@ -405,22 +424,16 @@ async def submit_banner(self, ctx, *args): await ctx.send("You cannot submit banners if you have the Trash Tier Banner Submissions role") return - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("BannerContestInfo",)) - - fetched = c.fetchone() - if not fetched: + banner_dict: dict | None = await self.get_banner_contest_info() + if banner_dict is None: await ctx.send("No banner contest is currently set") return - banner_dict = json.loads(fetched[0]) - timestamp = banner_dict["timestamp"] if not timestamp: await ctx.send("No banner contest is currently set") return - start_datetime = datetime.datetime.fromtimestamp(timestamp) - - if datetime.datetime.now() < start_datetime: + if datetime.datetime.now() < (start_datetime := datetime.datetime.fromtimestamp(timestamp)): await ctx.send(f"You must wait for {start_datetime.strftime('%Y-%m-%d %H:%M')} to submit!") return @@ -430,9 +443,9 @@ async def submit_banner(self, ctx, *args): ) return - stretch = "-stretch" in args or "-stretched" in args + stretch: bool = "-stretch" in args or "-stretched" in args - url = None + url: str | None = None if (stretch and len(args) == 1) or (not stretch and len(args) == 0): try: url = ctx.message.attachments[0].url @@ -447,10 +460,14 @@ async def submit_banner(self, ctx, *args): if stretch and len(args) > 2 or not stretch and len(args) > 1: await ctx.send("Too many arguments or misspelled flag") return - else: - for arg in args: - if arg != "-stretch" and arg != "-stretched": - url = arg + + # If the arguments look good: pull the URL out (a non-flag argument) + for arg in args: + if arg != "-stretch" and arg != "-stretched": + url = arg + + if url is None: + return user_image_file = BytesIO(requests.get(url).content) if user_image_file.getbuffer().nbytes >= 10000000: @@ -458,87 +475,92 @@ async def submit_banner(self, ctx, *args): return try: - with Image.open("./data/premade/banner_converter.png") as overlay_mask, Image.open( - "./data/premade/banner_preview.png" - ) as preview_mask, Image.open(user_image_file) as user_image: - + with ( + Image.open(Banner.CONVERTER_FILE) as overlay_mask, + Image.open(Banner.PREVIEW_FILE) as preview_mask, + Image.open(user_image_file) as user_image, + ): animated = user_image.is_animated overlay_mask_user_canvas_size = overlay_mask.size - preview_mask_user_canvas_size = (240, 135) - preview_mask_user_box_start = (5, 5) - overlay_frames = [] - preview_frames = [] + overlay_frames: list[Image] = [] + preview_frames: list[Image] = [] + durations: list[float] = [] + if animated: - durations = [] await ctx.send("Converting animated banner, this may take some time...") for frame in ImageSequence.Iterator(user_image): if animated: durations.append(frame.info["duration"]) + frame = frame.copy() rgba_frame = frame.convert("RGBA") + + # If we're stretching the banner, things are a bit easier for us, as we don't have to find a + # good 'fit' for the image. if stretch: overlay_frames.append( Image.alpha_composite(rgba_frame.resize(overlay_mask_user_canvas_size), overlay_mask) ) - preview_user_image = Image.new("RGBA", preview_mask.size, (0, 0, 0, 0)) + preview_user_image = Image.new("RGBA", preview_mask.size, Banner.TRANSPARENT) preview_user_image.paste( - rgba_frame.resize(preview_mask_user_canvas_size), preview_mask_user_box_start + rgba_frame.resize(Banner.PREVIEW_MASK_USER_CANVAS_SIZE), Banner.PREVIEW_MASK_USER_BOX_START ) preview_frames.append(Image.alpha_composite(preview_user_image, preview_mask)) - else: - overlay_ratio = max( - overlay_mask_user_canvas_size[0] / rgba_frame.size[0], - overlay_mask_user_canvas_size[1] / rgba_frame.size[1], - ) - overlay_user_image = Image.new("RGBA", overlay_mask_user_canvas_size, (0, 0, 0, 0)) - overlay_user_size = ( - int(rgba_frame.size[0] * overlay_ratio), - int(rgba_frame.size[1] * overlay_ratio), - ) - overlay_mask_user_image_start = ( - int(overlay_mask_user_canvas_size[0] / 2 - overlay_user_size[0] / 2), - int(overlay_mask_user_canvas_size[1] / 2 - overlay_user_size[1] / 2), - ) - overlay_user_image.paste(rgba_frame.resize(overlay_user_size), overlay_mask_user_image_start) - overlay_frames.append(Image.alpha_composite(overlay_user_image, overlay_mask)) + continue + + # Otherwise, we have to fit the image nicely into the frame + overlay_ratio: float = Banner.calc_ratio_max(overlay_mask_user_canvas_size, rgba_frame) + overlay_user_image: Image = Image.new("RGBA", overlay_mask_user_canvas_size, Banner.TRANSPARENT) + overlay_user_size: tuple[int, int] = ( + int(rgba_frame.size[0] * overlay_ratio), + int(rgba_frame.size[1] * overlay_ratio), + ) + overlay_mask_user_image_start: tuple[int, int] = ( + int(overlay_mask_user_canvas_size[0] / 2 - overlay_user_size[0] / 2), + int(overlay_mask_user_canvas_size[1] / 2 - overlay_user_size[1] / 2), + ) + overlay_user_image.paste(rgba_frame.resize(overlay_user_size), overlay_mask_user_image_start) + overlay_frames.append(Image.alpha_composite(overlay_user_image, overlay_mask)) + + preview_ratio: float = Banner.calc_ratio_max(Banner.PREVIEW_MASK_USER_CANVAS_SIZE, rgba_frame) + preview_user_image: Image = Image.new("RGBA", preview_mask.size, Banner.TRANSPARENT) + preview_user_size: tuple[int, int] = ( + int(rgba_frame.size[0] * preview_ratio), + int(rgba_frame.size[1] * preview_ratio), + ) + preview_mask_user_image_start: tuple[int, int] = ( + 5 + int(Banner.PREVIEW_MASK_USER_CANVAS_SIZE[0] / 2 - preview_user_size[0] / 2), + 5 + int(Banner.PREVIEW_MASK_USER_CANVAS_SIZE[1] / 2 - preview_user_size[1] / 2), + ) + preview_user_image.paste(rgba_frame.resize(preview_user_size), preview_mask_user_image_start) + preview_frames.append(Image.alpha_composite(preview_user_image, preview_mask)) - preview_ratio = max( - preview_mask_user_canvas_size[0] / rgba_frame.size[0], - preview_mask_user_canvas_size[1] / rgba_frame.size[1], - ) - preview_user_image = Image.new("RGBA", preview_mask.size, (0, 0, 0, 0)) - preview_user_size = ( - int(rgba_frame.size[0] * preview_ratio), - int(rgba_frame.size[1] * preview_ratio), - ) - preview_mask_user_image_start = ( - 5 + int(preview_mask_user_canvas_size[0] / 2 - preview_user_size[0] / 2), - 5 + int(preview_mask_user_canvas_size[1] / 2 - preview_user_size[1] / 2), - ) - preview_user_image.paste(rgba_frame.resize(preview_user_size), preview_mask_user_image_start) - preview_frames.append(Image.alpha_composite(preview_user_image, preview_mask)) except UnidentifiedImageError or requests.exceptions.MissingSchema: await ctx.send(f"Image couldn't be opened.") return replaced_message = False - c.execute("SELECT PreviewMessageID FROM BannerSubmissions WHERE UserID = ?", (ctx.author.id,)) - fetched = c.fetchone() + + fetched: tuple[int] | None = await self.fetch_one( + "SELECT PreviewMessageID FROM BannerSubmissions WHERE UserID = ?", + (ctx.author.id,), + ) + if fetched: try: message_to_replace = await self.banner_submissions_channel.fetch_message(fetched[0]) await message_to_replace.delete() except discord.errors.NotFound: await ctx.send( - f"Could not delete previously posted submission from {self.banner_submissions_channel.mention}. " - f"It might have been manually deleted." + f"Could not delete previously posted submission from " + f"{self.banner_submissions_channel.mention}. It might have been manually deleted." ) replaced_message = True - async def send_picture(frames, channel, filename): + async def send_picture(frames: list, channel: discord.TextChannel, filename: str) -> discord.Message: with BytesIO() as image_binary: if animated: frames[0].save( @@ -554,20 +576,22 @@ async def send_picture(frames, channel, filename): ) return message - filetype = "gif" if animated else "png" - converted_message = await send_picture( + filetype: str = "gif" if animated else "png" + converted_message: discord.Message = await send_picture( overlay_frames, self.banner_converted_channel, f"converted_banner.{filetype}" ) - preview_message = await send_picture( + preview_message: discord.Message = await send_picture( preview_frames, self.banner_submissions_channel, f"banner_preview.{filetype}" ) await preview_message.add_reaction(self.banner_vote_emoji) - c.execute( - "REPLACE INTO BannerSubmissions VALUES (?, ?, ?)", (ctx.author.id, preview_message.id, converted_message.id) - ) - conn.commit() - conn.close() + async with self.db() as db: + await db.execute( + "REPLACE INTO BannerSubmissions VALUES (?, ?, ?)", + (ctx.author.id, preview_message.id, converted_message.id), + ) + await db.commit() + await ctx.send(f"Banner successfully {'resubmitted' if replaced_message else 'submitted'}!") diff --git a/canary/cogs/base_cog.py b/canary/cogs/base_cog.py new file mode 100644 index 000000000..a20a8aafd --- /dev/null +++ b/canary/cogs/base_cog.py @@ -0,0 +1,100 @@ +import aiosqlite +import contextlib +import discord +import os + +from discord.ext import commands +from typing import Any, AsyncGenerator, Callable, Iterable + +from ..bot import Canary + +__all__ = [ + "CanaryCog", +] + + +class CanaryCog(commands.Cog): + def __init__(self, bot: Canary): + self.bot: Canary = bot + self.guild: discord.Guild | None = None + + @commands.Cog.listener() + async def on_ready(self): + self.guild = self.bot.get_guild(self.bot.config.server_id) + + # Make temporary directory, used mostly by images cog + if not os.path.exists("./tmp/"): + os.mkdir("./tmp/", mode=0o755) + + @contextlib.asynccontextmanager + async def db(self) -> AsyncGenerator[aiosqlite.Connection, None]: + async with self.bot.db() as conn: + yield conn + + async def fetch_list( + self, + query: str, + params: tuple[str | int | float | bool, ...] = (), + db: aiosqlite.Connection | None = None, + ) -> list[tuple]: + if fresh_db := (db is None): + db = await self.bot.db_nocm() + try: + async with db.execute(query, params) as c: + return [tuple(r) for r in await c.fetchall()] + finally: + if fresh_db: + await db.close() + + async def fetch_one( + self, + query: str, + params: tuple[str | int | float | bool, ...] = (), + db: aiosqlite.Connection | None = None, + ) -> tuple | None: + if fresh_db := (db is None): + db = await self.bot.db_nocm() + try: + async with db.execute(query, params) as c: + return await c.fetchone() + finally: + if fresh_db: + await db.close() + + async def get_settings_key(self, key: str, deserialize: Callable[[str], Any] = str) -> Any | None: + db: aiosqlite.Connection + async with self.db() as db: + c: aiosqlite.Cursor + async with db.execute("SELECT Value FROM Settings WHERE Key = ?", (key,)) as c: + fetched = await c.fetchone() + return deserialize(fetched[0]) if fetched is not None else None + + async def set_settings_key( + self, + key: str, + value: Any, + serialize: Callable[[Any], str] = str, + pre_commit: Iterable | None = None, + ) -> None: + db: aiosqlite.Connection + async with self.db() as db: + c: aiosqlite.Cursor + await db.execute("REPLACE INTO Settings VALUES (?, ?)", (key, serialize(value))) + + if pre_commit: + for s in pre_commit: + await db.execute(s) + + await db.commit() + + async def del_settings_key(self, key: str, pre_commit: Iterable | None = None) -> None: + db: aiosqlite.Connection + async with self.db() as db: + c: aiosqlite.Cursor + await db.execute("DELETE FROM Settings WHERE Key = ? LIMIT 1", (key,)) + + if pre_commit: + for s in pre_commit: + await db.execute(s) + + await db.commit() diff --git a/cogs/currency.py b/canary/cogs/currency.py similarity index 52% rename from cogs/currency.py rename to canary/cogs/currency.py index d2f9c05b6..d964b176a 100644 --- a/cogs/currency.py +++ b/canary/cogs/currency.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2019) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -17,30 +15,24 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -# discord.py requirements -import discord -from discord.ext import commands - -# For type hinting -from typing import Dict, List, Optional, Tuple - -# For DB functionality -import sqlite3 import datetime -from .utils.members import add_member_if_needed -# For tables -from tabulate import tabulate -from .utils.paginator import Pages +import aiosqlite +import discord +import json +import random -# For general currency shenanigans from decimal import Decimal, InvalidOperation +from discord.ext import commands +from tabulate import tabulate +from typing import Optional -# For betting -import random +from ..bot import Canary +from ..config.config import CurrencyModel +from .base_cog import CanaryCog +from .utils.members import add_member_if_needed +from .utils.paginator import Pages -# For other stuff -import json CLAIM_AMOUNT = Decimal(20) CLAIM_WAIT_TIME = datetime.timedelta(hours=1) @@ -68,41 +60,37 @@ ) -class Currency(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.currency = self.bot.config.currency - self.prec = self.currency["precision"] - - async def fetch_all_balances(self) -> List[Tuple[str, str, Decimal]]: - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute( - "SELECT BT.UserID, M.Name, IFNULL(SUM(BT.Amount), 0) " - "FROM BankTransactions AS BT, Members as M " - "WHERE BT.UserID = M.ID GROUP BY UserID" - ) - - results = [(user_id, name, self.db_to_currency(balance)) for user_id, name, balance in c.fetchall()] - - conn.close() - - return results +class Currency(CanaryCog): + def __init__(self, bot: Canary): + super().__init__(bot) + self.currency: CurrencyModel = self.bot.config.currency + + self.symbol: str = self.currency.symbol + self.prec: int = self.currency.precision + self.initial: Decimal = Decimal(self.currency.initial) + + async def fetch_all_balances(self) -> list[tuple[str, str, Decimal]]: + # after + return [ + (user_id, name, self.db_to_currency(balance)) + for user_id, name, balance in ( + await self.fetch_list( + "SELECT BT.UserID, M.Name, IFNULL(SUM(BT.Amount), 0) " + "FROM BankTransactions AS BT, Members as M " + "WHERE BT.UserID = M.ID GROUP BY UserID" + ) + ) + ] async def fetch_bank_balance(self, user: discord.Member) -> Decimal: - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT IFNULL(SUM(Amount), 0) FROM BankTransactions WHERE " "UserID = ?", (user.id,)) - - balance = self.db_to_currency(c.fetchone()[0]) - if balance is None: - balance = Decimal(0) - - conn.close() - - return balance + balance_t = await self.fetch_one( + "SELECT IFNULL(SUM(Amount), 0) FROM BankTransactions WHERE UserID = ?", (user.id,) + ) + return self.db_to_currency(balance_t[0]) if balance_t is not None else Decimal(0) - async def create_bank_transaction(self, c, user: discord.Member, amount: Decimal, action: str, metadata: Dict): + async def create_bank_transaction( + self, db: aiosqlite.Connection, user: discord.Member, amount: Decimal, action: str, metadata: dict + ) -> None: # Don't create another connection in this function in order to properly # transaction-ify a series of bank "transactions". @@ -112,34 +100,38 @@ async def create_bank_transaction(self, c, user: discord.Member, amount: Decimal now = int(datetime.datetime.now().timestamp()) - c.execute("PRAGMA foreign_keys = ON") - await add_member_if_needed(self, c, user.id) - t = (user.id, self.currency_to_db(amount), action, json.dumps(metadata), now) - c.execute("INSERT INTO BankTransactions(UserID, Amount, Action, " "Metadata, Date) VALUES(?, ?, ?, ?, ?)", t) + await db.execute("PRAGMA foreign_keys = ON") + await add_member_if_needed(self, db, user.id) + await db.execute( + "INSERT INTO BankTransactions(UserID, Amount, Action, Metadata, Date) VALUES(?, ?, ?, ?, ?)", + (user.id, self.currency_to_db(amount), action, json.dumps(metadata), now), + ) - def parse_currency(self, amount: str, balance: Decimal): + @staticmethod + def parse_currency(amount: str, balance: Decimal) -> Decimal | None: if amount.lower().strip() in CURRENCY_ALL: return balance - else: - try: - return Decimal(amount) - except InvalidOperation: - # Value error (invalid conversion) - return None - def currency_to_db(self, amount: Decimal): - return int(amount * Decimal(10 ** self.currency["precision"])) + try: + return Decimal(amount) + except InvalidOperation: + # Value error (invalid conversion) + return None - def db_to_currency(self, amount: int): - return Decimal(amount) / Decimal(10 ** self.currency["precision"]) + def currency_to_db(self, amount: Decimal) -> int: + return int(amount * Decimal(10**self.prec)) - def format_currency(self, amount: Decimal): + def db_to_currency(self, amount: int) -> Decimal: + return Decimal(amount) / Decimal(10**self.prec) + + def format_currency(self, amount: Decimal) -> str: return ("{:." + str(self.prec) + "f}").format(amount) - def format_symbol_currency(self, amount: Decimal): - return self.currency["symbol"] + self.format_currency(amount) + def format_symbol_currency(self, amount: Decimal) -> str: + return self.symbol + self.format_currency(amount) - def check_bet(self, balance: Decimal, bet: Decimal) -> Optional[str]: + @staticmethod + def check_bet(balance: Decimal, bet: Decimal) -> str | None: """ Checks universally invalid betting cases. """ @@ -161,51 +153,49 @@ def check_bet(self, balance: Decimal, bet: Decimal) -> Optional[str]: if bet > balance: return "You're too broke to bet that much!" - return "" + async def get_last_claim_time(self, db: aiosqlite.Connection, author: discord.Member | discord.User) -> int | None: + claim_time_t = await self.fetch_one( + "SELECT IFNULL(MAX(Date), 0) FROM BankTransactions WHERE UserID = ? AND Action = ?", + (author.id, ACTION_INITIAL_CLAIM), + db=db, + ) + return claim_time_t[0] if claim_time_t is not None else None @commands.command() - async def initial_claim(self, ctx): + async def initial_claim(self, ctx: commands.Context): """ Claim's the user's start-up grant currency. """ - # Start bot typing await ctx.trigger_typing() - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + author = ctx.message.author + author_name = author.display_name - c.execute( - "SELECT IFNULL(MAX(Date), 0) FROM BankTransactions " "WHERE UserID = ? AND Action = ?", - (ctx.message.author.id, ACTION_INITIAL_CLAIM), - ) + db: aiosqlite.Connection + async with self.db() as db: + claim_time = await self.get_last_claim_time(db, author) - claim_time = c.fetchone()[0] + if claim_time is None: + return - author_name = ctx.message.author.display_name - - if claim_time > 0: - await ctx.send("{} has already claimed their initial " "currency.".format(author_name)) - return - - metadata = {"channel": ctx.message.channel.id} + if claim_time > 0: + await ctx.send(f"{author_name} has already claimed their initial currency.") + return - await self.create_bank_transaction( - c, ctx.message.author, self.currency["initial_amount"], ACTION_INITIAL_CLAIM, metadata - ) - - conn.commit() - - await ctx.send( - "{} claimed their initial {}!".format( - author_name, self.format_symbol_currency(self.currency["initial_amount"]) + await self.create_bank_transaction( + db, + author, + self.initial, + ACTION_INITIAL_CLAIM, + {"channel": ctx.message.channel.id}, ) - ) + await db.commit() - conn.close() + await ctx.send(f"{author_name} claimed their initial {self.format_symbol_currency(self.initial)}!") @commands.command() - async def claim(self, ctx): + async def claim(self, ctx: commands.Context): """ Claim's the user's hourly currency. """ @@ -213,38 +203,32 @@ async def claim(self, ctx): # Start bot typing await ctx.trigger_typing() - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - - c.execute( - "SELECT IFNULL(MAX(Date), 0) FROM BankTransactions " "WHERE UserID = ? AND Action = ?", - (ctx.message.author.id, ACTION_CLAIM), - ) - - last_claimed = datetime.datetime.fromtimestamp(c.fetchone()[0]) + author = ctx.message.author threshold = datetime.datetime.now() - CLAIM_WAIT_TIME - if last_claimed < threshold: - author_name = ctx.message.author.display_name if ctx.message.author else ":b:roken bot" - - metadata = {"channel": ctx.message.channel.id} + db: aiosqlite.Connection + async with self.db() as db: + claim_time = await self.get_last_claim_time(db, author) - await self.create_bank_transaction(c, ctx.message.author, CLAIM_AMOUNT, ACTION_CLAIM, metadata) + if claim_time is None: + return - conn.commit() + last_claimed = datetime.datetime.fromtimestamp(claim_time) - await ctx.send("{} claimed {}!".format(author_name, self.format_symbol_currency(CLAIM_AMOUNT))) + if last_claimed < threshold: + metadata = {"channel": ctx.message.channel.id} + await self.create_bank_transaction(db, author, CLAIM_AMOUNT, ACTION_CLAIM, metadata) + await db.commit() - else: - time_left = last_claimed - threshold - await ctx.send( - "Please wait {}h {}m to claim again!".format(time_left.seconds // 3600, time_left.seconds // 60 % 60) - ) + author_name = author.display_name if author else ":b:roken bot" + await ctx.send(f"{author_name} claimed {self.format_symbol_currency(CLAIM_AMOUNT)}!") + return - conn.close() + time_left = last_claimed - threshold + await ctx.send(f"Please wait {time_left.seconds // 3600}h {time_left.seconds // 60 % 60}m to claim again!") @commands.command(aliases=["$", "bal"]) - async def balance(self, ctx, user: discord.Member = None): + async def balance(self, ctx: commands.Context, user: Optional[discord.Member] = None): """ Return the user's account balance. """ @@ -257,16 +241,16 @@ async def balance(self, ctx, user: discord.Member = None): author = user if user else ctx.message.author amount = self.format_symbol_currency(await self.fetch_bank_balance(author)) - await ctx.send("{} has {} in their account.".format(author.display_name, amount)) + await ctx.send(f"{author.display_name} has {amount} in their account.") @commands.command(aliases=["bf"]) - async def bet_flip(self, ctx, bet: str = None, face: str = None): + async def bet_flip(self, ctx: commands.Context, bet: str = "", face: str = ""): """ Bets an amount of money on a coin flip. - Usage: ?bet_flip h 10 or ?bet_flip t 5 + Usage: ?bet_flip 10 h or ?bet_flip 5 t """ - if face is None or bet is None: + if bet == "" or face == "": return # Start bot typing @@ -278,8 +262,7 @@ async def bet_flip(self, ctx, bet: str = None, face: str = None): # Handle invalid cases - error = self.check_bet(balance, bet_dec) - if error != "": + if (error := self.check_bet(balance, bet_dec)) is not None: await ctx.send(error) return @@ -290,16 +273,14 @@ async def bet_flip(self, ctx, bet: str = None, face: str = None): # If all cases pass, perform the gamble - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - result = random.choice(COIN_FLIP_CHOICES) - metadata = {"result": result, "channel": ctx.message.channel.id} - amount = bet_dec if choice == result else -bet_dec - await self.create_bank_transaction(c, ctx.message.author, amount, ACTION_BET_FLIP, metadata) - conn.commit() + + db: aiosqlite.Connection + async with self.db() as db: + await self.create_bank_transaction(db, ctx.message.author, amount, ACTION_BET_FLIP, metadata) + await db.commit() message = "Sorry! {} lost {} (result was **{}**)." if choice == result: @@ -309,39 +290,33 @@ async def bet_flip(self, ctx, bet: str = None, face: str = None): await ctx.send(message.format(author_name, self.format_symbol_currency(bet_dec), result)) - conn.close() - @commands.command(aliases=["br"]) - async def bet_roll(self, ctx, bet: str = None): + async def bet_roll(self, ctx: commands.Context, bet: str = ""): """ Bets an amount of currency on a D100 roll. Usage: ?bet_roll 100 or ?br all """ - if bet is None: + if bet == "": return # Start bot typing await ctx.trigger_typing() - balance = await self.fetch_bank_balance(ctx.message.author) + balance: Decimal = await self.fetch_bank_balance(ctx.message.author) bet_dec = self.parse_currency(bet, balance) # Handle invalid cases - error = self.check_bet(balance, bet_dec) - if error != "": + if (error := self.check_bet(balance, bet_dec)) is not None: await ctx.send(error) return # If all cases pass, perform the gamble - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - result = random.randrange(1, 101) amount_returned = Decimal(0) - for case, amount in self.currency["bet_roll_cases"]: + for case, amount in zip(self.currency.bet_roll_cases, self.currency.bet_roll_returns): if result <= case: amount_returned = bet_dec * amount break @@ -353,27 +328,29 @@ async def bet_roll(self, ctx, bet: str = None): "channel": ctx.message.channel.id, } - await self.create_bank_transaction(c, ctx.message.author, amount_returned - bet_dec, ACTION_BET_ROLL, metadata) - - conn.commit() + db: aiosqlite.Connection + async with self.db() as db: + await self.create_bank_transaction( + db, ctx.message.author, amount_returned - bet_dec, ACTION_BET_ROLL, metadata + ) + await db.commit() - message = "Sorry! {un} lost {am} (result was **{re}**)." if amount_returned == bet_dec: - message = "{un} broke even (result was **{re}**)." + message_tpl = "{un} broke even (result was **{re}**)." elif amount_returned > bet_dec: - message = "Congratulations! {un} won [net] {am} (result was " "**{re}**)." + message_tpl = "Congratulations! {un} won [net] {am} (result was **{re}**)." + else: + message_tpl = "Sorry! {un} lost {am} (result was **{re}**)." author_name = ctx.message.author.display_name amount_msg_multiplier = -1 if amount_returned < bet_dec else 1 bet_str = self.format_symbol_currency(amount_msg_multiplier * (amount_returned - bet_dec)) - await ctx.send(message.format(un=author_name, am=bet_str, re=result)) - - conn.close() + await ctx.send(message_tpl.format(un=author_name, am=bet_str, re=result)) @commands.command() - async def give(self, ctx, user: discord.Member = None, amount: str = None): + async def give(self, ctx: commands.Context, user: discord.Member, amount: str = ""): """ Gives some amount of currency to another user. """ @@ -381,11 +358,11 @@ async def give(self, ctx, user: discord.Member = None, amount: str = None): # Start bot typing await ctx.trigger_typing() - if not user or amount is None: + if amount == "": await ctx.send("Usage: ?give [user] [amount]") return - balance = await self.fetch_bank_balance(ctx.message.author) + balance: Decimal = await self.fetch_bank_balance(ctx.message.author) if balance <= 0: await ctx.send("You're too broke to give anyone anything!") @@ -396,11 +373,11 @@ async def give(self, ctx, user: discord.Member = None, amount: str = None): # Handle invalid cases if amount_dec is None: - await ctx.send("Invalid quantity: '{}'.".format(amount)) + await ctx.send(f"Invalid quantity: '{amount}'.") return if amount_dec <= 0: - await ctx.send("You cannot give {}!".format(self.format_symbol_currency(amount_dec))) + await ctx.send(f"You cannot give {self.format_symbol_currency(amount_dec)}!") return if amount_dec > balance: @@ -417,24 +394,18 @@ async def give(self, ctx, user: discord.Member = None, amount: str = None): gen = user.display_name gifter_metadata = {"giftee": user.id, "channel": ctx.message.channel.id} - giftee_metadata = {"gifter": ctx.message.author.id, "channel": ctx.message.channel.id} - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - - await self.create_bank_transaction(c, ctx.message.author, -amount_dec, ACTION_GIFTER, gifter_metadata) - - await self.create_bank_transaction(c, user, amount_dec, ACTION_GIFTEE, giftee_metadata) - - conn.commit() - - await ctx.send("{} gave {} to {}!".format(grn, self.format_symbol_currency(amount_dec), gen)) + db: aiosqlite.Connection + async with self.db() as db: + await self.create_bank_transaction(db, ctx.message.author, -amount_dec, ACTION_GIFTER, gifter_metadata) + await self.create_bank_transaction(db, user, amount_dec, ACTION_GIFTEE, giftee_metadata) + await db.commit() - conn.close() + await ctx.send(f"{grn} gave {self.format_symbol_currency(amount_dec)} to {gen}!") @commands.command(aliases=["lb"]) - async def leaderboard(self, ctx): + async def leaderboard(self, ctx: commands.Context): """ Currency rankings """ @@ -444,14 +415,14 @@ async def leaderboard(self, ctx): balances = sorted(await self.fetch_all_balances(), reverse=True, key=lambda b: b[2]) if len(balances) == 0: - await ctx.send("Leaderboards are not yet available for this server, please " "collect some currency.") + await ctx.send("Leaderboards are not yet available for this server, please collect some currency.") return table = [] table_list = [] counter = 1 - for (_user_id, name, balance) in balances: + for _user_id, name, balance in balances: table.append((counter, name, self.format_symbol_currency(balance))) if counter % 7 == 0 or counter == len(balances): table_list.append(tabulate(table[:counter], headers=["Rank", "Name", "Balance"], tablefmt="fancy_grid")) diff --git a/canary/cogs/customreactions.py b/canary/cogs/customreactions.py new file mode 100644 index 000000000..754c602d2 --- /dev/null +++ b/canary/cogs/customreactions.py @@ -0,0 +1,960 @@ +# Copyright (C) idoneam (2016-2023) +# +# This file is part of Canary +# +# Canary is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Canary is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Canary. If not, see . + +import asyncio +import aiosqlite +import discord + +from discord.ext import commands +from typing import Callable, Coroutine, Literal, TypedDict + +from ..bot import Canary +from .base_cog import CanaryCog +from .utils.paginator import Pages +from .utils.p_strings import PStringEncodings + + +class AssistantAction(TypedDict, total=False): + fn: Callable[[discord.Message, bool, ...], Coroutine[None, None, bool]] + desc: str + kwargs: dict + hidden: bool + + +EMOJI = { + "new": "🆕", + "mag": "🔍", + "pencil": "📝", + "stop_button": "⏹", + "ok": "🆗", + "white_check_mark": "✅", + "x": "❌", + "put_litter_in_its_place": "🚮", + "rewind": "⏪", + "arrow_backward": "◀", + "arrow_forward": "▶", + "fast_forward": "⏩", + "grey_question": "❔", + "zero": "0️⃣", + "one": "1️⃣", + "two": "2️⃣", + "three": "3️⃣", + "four": "4️⃣", + "five": "5️⃣", +} + +NUMBERS = (EMOJI["zero"], EMOJI["one"], EMOJI["two"], EMOJI["three"], EMOJI["four"], EMOJI["five"]) + +CUSTOM_REACTION_TIMEOUT = "Custom Reaction timed out. You may want to run the command again." +STOP_TEXT = "stop" +LOADING_EMBED = discord.Embed(title="Loading...") + + +class CustomReactions(CanaryCog): + # Written by @le-potate + def __init__(self, bot: Canary): + super().__init__(bot) + + self.reaction_list: list[tuple] = [] + self.proposal_list: list[tuple] = [] + self.p_strings: PStringEncodings | None = None + + @CanaryCog.listener() + async def on_ready(self): + await super().on_ready() + await self.rebuild_lists() + + async def rebuild_lists(self) -> None: + await self.rebuild_reaction_list() + await self.rebuild_proposal_list() + + async def rebuild_reaction_list(self) -> None: + self.reaction_list = await self.fetch_list("SELECT * FROM CustomReactions WHERE Proposal = 0") + + prompts = [row[1].lower() for row in self.reaction_list] + responses = [row[2] for row in self.reaction_list] + anywhere_values = [row[5] for row in self.reaction_list] + additional_info_list = [(row[4], row[6]) for row in self.reaction_list] + self.p_strings = PStringEncodings( + prompts, responses, anywhere_values, additional_info_list=additional_info_list + ) + + async def rebuild_proposal_list(self) -> None: + self.proposal_list = await self.fetch_list("SELECT * FROM CustomReactions WHERE Proposal = 1") + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + if message.author == self.bot.user: + return + + if self.p_strings is None: + return + + response = self.p_strings.parser( + message.content.lower(), user=message.author.mention, channel=str(message.channel) + ) + if response: + # delete the prompt if DeletePrompt option is activated + if response.additional_info[0] == 1: + await message.delete() + + # send the response if DM option selected, + # send in the DM of the user who wrote the prompt + if response.additional_info[1] == 1: + await message.author.send(str(response)) + else: + await message.channel.send(str(response)) + + @commands.max_concurrency(1, per=commands.BucketType.user, wait=False) + @commands.command(aliases=["customreaction", "customreacts", "customreact"]) + async def customreactions(self, ctx: commands.Context): + current_options = [] + main_user = ctx.message.author + await ctx.message.delete() + + def get_number_of_proposals() -> int: + return len(self.proposal_list) + + def get_reaction_check(moderators=False, reaction_user=None) -> Callable: + def reaction_check(reaction, user): + return all( + ( + reaction.emoji in current_options, + reaction.message.id == initial_message.id, + not moderators or discord.utils.get(user.roles, name=self.bot.config.moderator_role), + not reaction_user or user == reaction_user, + ) + ) + + return reaction_check + + def get_msg_check(msg_user=None) -> Callable: + def msg_check(msg): + if all((not msg_user or msg.author == msg_user, msg.channel == ctx.channel)): + if msg.attachments: + # in python 3.7, rewrite as + # asyncio.create_task(ctx.send([...])) + # (the get_event_loop() part isn't necessary) + loop = asyncio.get_event_loop() + loop.create_task(ctx.send("Attachments cannot be used, but you may use URLs")) + else: + return True + + return msg_check + + def get_number_check(msg_user=None, number_range=None) -> Callable: + def number_check(msg): + if msg.content.isdigit(): + return all( + ( + not msg_user or msg.author == msg_user, + not number_range or int(msg.content) in number_range, + msg.channel == ctx.channel, + ) + ) + + return number_check + + async def wait_for_reaction(message: discord.Message): + try: + reaction, user = await self.bot.wait_for( + "reaction_add", check=get_reaction_check(reaction_user=main_user), timeout=60 + ) + except asyncio.TimeoutError: + await message.clear_reactions() + await message.edit(embed=discord.Embed(title=CUSTOM_REACTION_TIMEOUT), delete_after=60) + return + return reaction, user + + async def wait_for_message(message: discord.Message) -> str | None: + try: + msg = await self.bot.wait_for("message", check=get_msg_check(msg_user=main_user), timeout=60) + except asyncio.TimeoutError: + await message.clear_reactions() + await message.edit(embed=discord.Embed(title=CUSTOM_REACTION_TIMEOUT), delete_after=60) + return + content = msg.content + await msg.delete() + return content + + async def clear_options(message: discord.Message) -> None: + current_options.clear() + await message.clear_reactions() + + async def add_multiple_reactions(message: discord.Message, reactions) -> None: + for reaction in reactions: + await message.add_reaction(reaction) + + async def add_yes_or_no_reactions(message: discord.Message) -> None: + await add_multiple_reactions(message, (EMOJI["zero"], EMOJI["one"], EMOJI["stop_button"])) + + async def add_control_reactions(message: discord.Message) -> None: + await add_multiple_reactions( + message, + ( + EMOJI["rewind"], + EMOJI["arrow_backward"], + EMOJI["arrow_forward"], + EMOJI["fast_forward"], + EMOJI["stop_button"], + EMOJI["ok"], + ), + ) + + async def create_assistant(message: discord.Message, is_moderator: bool) -> bool: + actions: dict[str, AssistantAction] = { + # Add/Propose a new custom reaction + EMOJI["new"]: { + "fn": add_custom_react, + "desc": f"{'Add' if is_moderator else 'Propose'} a new custom reaction", + "kwargs": dict(proposals=not is_moderator), + }, + # List custom reactions + EMOJI["mag"]: { + "fn": list_custom_reacts, + "desc": "See the list of current reactions" + (" and modify them" if is_moderator else ""), + "kwargs": dict(proposals=False), + }, + # List proposals + EMOJI["pencil"]: { + "fn": list_custom_reacts, + "desc": f"See the list of proposed reactions ({get_number_of_proposals()})" + + (" and approve or reject them" if is_moderator else ""), + "kwargs": dict(proposals=True), + }, + # List placeholders + EMOJI["grey_question"]: { + "fn": list_placeholders, + "desc": "List of placeholders", + "kwargs": dict(), + }, + # Stop + EMOJI["stop_button"]: { + "fn": leave, + "desc": "", + "kwargs": dict(), + "hidden": True, + }, + } + + description = "\n".join(f"{k} {v['desc']}" for k, v in actions.items() if not v.get("hidden")) + + action_keys = tuple(actions.keys()) + + current_options.extend(action_keys) + await add_multiple_reactions(message, action_keys) + + await message.edit( + embed=discord.Embed(title="Custom Reactions", description=description).set_footer( + text=f"{main_user}: Click on an emoji to choose an " + f"option (If a list is chosen, all users " + f"will be able to interact with it)", + icon_url=main_user.avatar_url, + ) + ) + + try: + reaction, user = await wait_for_reaction(message) + except TypeError: + return False + + await clear_options(message) + + if (action := actions.get(reaction.emoji)) is not None: + return await action["fn"](message, **action["kwargs"]) + + async def add_custom_react(message: discord.Message, proposals: bool) -> bool: + status_msg = f"{main_user} is currently {'proposing' if proposals else 'adding'} a custom reaction." + + title = f"{'Propose' if proposals else 'Add'} a custom reaction" + footer = f"{status_msg}\nWrite '{STOP_TEXT}' to cancel." + description = "Write the prompt the bot will react to" + + async def _refresh_msg(): + await message.edit( + embed=discord.Embed(title=title, description=description).set_footer( + text=footer, icon_url=main_user.avatar_url + ) + ) + + await _refresh_msg() + + prompt_message = await wait_for_message(message) + + if prompt_message is None: + return False + + if prompt_message.lower() == STOP_TEXT: + await leave(message) + return True + + description = f"Prompt: {prompt_message}\nWrite the response the bot will send" + await _refresh_msg() + + response = await wait_for_message(message) + + if response is None: + return False + + if response.lower() == STOP_TEXT: + await leave(message) + return True + + await message.edit(embed=LOADING_EMBED) + + description = ( + f"Prompt: {prompt_message}\nResponse: {response}\n" + f"React with the options you want and click {EMOJI['ok']} when you are ready\n" + f"{EMOJI['one']} Delete the message that calls the reaction\n" + f"{EMOJI['two']} Activate the custom reaction if the prompt is anywhere in a message \n" + f"{EMOJI['three']} React in the DMs of the user who calls the reaction instead of the channel\n" + ) + + footer = f"{main_user} is currently {'proposing' if proposals else 'adding'} a custom reaction." + + current_options.extend((EMOJI["ok"], EMOJI["stop_button"])) + await add_multiple_reactions(message, (*NUMBERS[1:4], EMOJI["ok"], EMOJI["stop_button"])) + await _refresh_msg() + + try: + reaction, user = await wait_for_reaction(message) + except TypeError: + return False + + # If the user clicked OK, check if delete/anywhere/dm are checked + if reaction.emoji == EMOJI["ok"]: + delete = False + anywhere = False + dm = False + cache_msg = await message.channel.fetch_message(message.id) + for reaction in cache_msg.reactions: + users_who_reacted = await reaction.users().flatten() + if main_user in users_who_reacted: + delete = delete or reaction.emoji == EMOJI["one"] + anywhere = anywhere or reaction.emoji == EMOJI["two"] + dm = dm or reaction.emoji == EMOJI["three"] + + current_options.clear() + await message.clear_reactions() + + db: aiosqlite.Connection + async with self.db() as db: + await db.execute( + "INSERT INTO CustomReactions(Prompt, Response, UserID, DeletePrompt, Anywhere, DM, Proposal) " + "VALUES(?, ?, ?, ?, ?, ?, ?)", + (prompt_message, response, main_user.id, delete, anywhere, dm, proposals), + ) + await db.commit() + + await self.rebuild_lists() + + if proposals: + title = "Custom reaction proposal successfully submitted!" + else: + title = "Custom reaction successfully added!" + description = f"-Prompt: {prompt_message}\n-Response: {response}" + if delete: + description = f"{description}\n-Will delete the message that calls the reaction" + if anywhere: + description = ( + f"{description}\n-Will activate the custom reaction if the prompt is anywhere in a message" + ) + if dm: + description = ( + f"{description}\n" + f"-Will react in the DMs of the user who calls the reaction instead of the channel" + ) + + await _refresh_msg() + + return False + + # Stop + if reaction.emoji == EMOJI["stop_button"]: + return await leave(message) + + return False + + async def list_custom_reacts(message: discord.Message, proposals: bool) -> bool: + current_list = self.proposal_list if proposals else self.reaction_list + + no_items_msg = f"There are currently no custom reaction{' proposal' if proposals else ''}s in this server" + + if not current_list: + title = no_items_msg + await message.edit(embed=discord.Embed(title=title), delete_after=60) + return False + + reaction_dict = { + "names": [f"[{i + 1}]" for i in range(len(current_list))], + "values": [ + f"Prompt: {reaction[1][:min(len(reaction[1]), 287)]}" + f'{"..." if len(reaction[1]) > 287 else ""}\n' + f"Response: {reaction[2][:min(len(reaction[2]), 287)]}" + f'{"..." if len(reaction[2]) > 287 else ""}' + for reaction in current_list + ], + } + + await message.edit(embed=LOADING_EMBED) + + await add_control_reactions(message) + + if proposals: + title = ( + f"Current custom reaction proposals\n" + f"Click on {EMOJI['ok']} to approve, reject, edit, or see more information on one of them" + ) + else: + title = ( + f"Current custom reactions\n" + f"Click on {EMOJI['ok']} to edit or see more information on one of them" + ) + + p = Pages( + ctx, + msg=message, + item_list=reaction_dict, + title=title, + display_option=(2, 10), + editable_content_emoji=EMOJI["ok"], + return_user_on_edit=True, + ) + + user_modifying = await p.paginate() + while p.edit_mode: + await message.clear_reactions() + if proposals: + title = ( + f"Current custom reaction proposals\n" + f"{user_modifying}: Write the number of the custom reaction proposal you want to " + f"approve, reject, edit, or see more information on" + ) + else: + title = ( + f"Current custom reactions\n" + f"{user_modifying}: Write the number of the custom reaction you want to edit or see more " + f"information on" + ) + message.embeds[0].title = title + await message.edit(embed=message.embeds[0]) + number = 0 + try: + msg = await self.bot.wait_for( + "message", + check=get_number_check(msg_user=user_modifying, number_range=range(1, len(current_list) + 1)), + timeout=60, + ) + number = int(msg.content) + await msg.delete() + except asyncio.TimeoutError: + pass + + if number == 0: + if proposals: + title = ( + f"Current custom reaction proposals\n" + f"Click on {EMOJI['ok']} to approve, reject, edit, or see more information on one of them " + f"(Previous attempt received invalid input or timed out)" + ) + else: + title = ( + f"Current custom reactions\n" + f"Click on {EMOJI['ok']} to edit or see more information on one of " + f"them (Previous attempt received invalid input or timed out)" + ) + p = Pages( + ctx, + msg=message, + item_list=reaction_dict, + title=title, + display_option=(2, 10), + editable_content_emoji=EMOJI["ok"], + return_user_on_edit=True, + ) + + else: + if left := await information_on_react(message, current_list, number, proposals): + return left + + if proposals: + title = ( + f"Current custom reaction proposals\n" + f"Click on {EMOJI['ok']} to approve, reject, edit, or see more information on one of them" + ) + else: + title = ( + f"Current custom reactions\n" + f"Click on {EMOJI['ok']} to edit or see more information on one of them" + ) + + # update dictionary since a custom reaction might have been + # modified + current_list = self.proposal_list if proposals else self.reaction_list + + if not current_list: + title = no_items_msg + await message.edit(embed=discord.Embed(title=title), delete_after=60) + return False + + reaction_dict = { + "names": [f"[{i + 1}]" for i in range(len(current_list))], + "values": [ + f"Prompt: {reaction[1][:min(len(reaction[1]), 287)]}" + f'{"..." if len(reaction[1]) > 287 else ""}' + f"\nResponse: {reaction[2][:min(len(reaction[2]), 287)]}" + f'{"..." if len(reaction[2]) > 287 else ""}' + for reaction in current_list + ], + } + + p = Pages( + ctx, + msg=message, + item_list=reaction_dict, + title=title, + display_option=(2, 10), + editable_content_emoji=EMOJI["ok"], + return_user_on_edit=True, + ) + + await message.edit(embed=LOADING_EMBED) + await add_control_reactions(message) + user_modifying = await p.paginate() + + return False + + async def information_on_react(message: discord.Message, current_list, number, proposals) -> bool: + await message.edit(embed=LOADING_EMBED) + + custom_react = current_list[number - 1] + prompt = custom_react[1] + response = custom_react[2] + user_who_added = self.bot.get_user(custom_react[3]) + delete = custom_react[4] + anywhere = custom_react[5] + dm = custom_react[6] + + delete_str = f"{'Deletes' if delete == 1 else 'Does not delete'} the message that calls the reaction" + + if anywhere == 1: + anywhere_str = "Activates the custom reaction if the prompt is anywhere in a message" + else: + anywhere_str = "Only activates the custom reaction if the prompt is the full message" + + if dm == 1: + dm_str = "Reacts in the DMs of the user who calls the reaction instead of the channel" + else: + dm_str = "Reacts directly into the channel" + + base_desc = ( + f"{EMOJI['one']} Prompt: {prompt}\n" + f"{EMOJI['two']} Response: {response}\n" + f"{EMOJI['three']} {delete_str}\n" + f"{EMOJI['four']} {anywhere_str}\n" + f"{EMOJI['five']} {dm_str}\n" + ) + + if proposals: + description = ( + f"{base_desc}\n" + f"{EMOJI['white_check_mark']} Approve this proposal\n" + f"{EMOJI['x']} Reject this proposal\n" + f"Added by {user_who_added}" + ) + title = ( + f"More information on a custom reaction proposal.\n" + f"{self.bot.config.moderator_role}s " + f"may click on emojis to modify those values or " + f"approve/refuse this proposal\n" + f"(Will return to the list of current reaction " + f"proposals in 40 seconds otherwise)" + ) + else: + description = ( + f"{base_desc}\n" + f"{EMOJI['put_litter_in_its_place']} Delete this custom reaction\n" + f"Added by {user_who_added}" + ) + title = ( + f"More information on a custom reaction.\n" + f"{self.bot.config.moderator_role}s may click " + f"on emojis to modify those values " + f"or select an option\n(Will return to the list of " + f"current reactions in 40 seconds otherwise)" + ) + + await clear_options(message) + if proposals: + current_options.extend((*NUMBERS[1:6], EMOJI["white_check_mark"], EMOJI["x"], EMOJI["stop_button"])) + else: + current_options.extend((*NUMBERS[1:6], EMOJI["put_litter_in_its_place"], EMOJI["stop_button"])) + if proposals: + await add_multiple_reactions( + message, (*NUMBERS[1:6], EMOJI["white_check_mark"], EMOJI["x"], EMOJI["stop_button"]) + ) + else: + await add_multiple_reactions( + message, (*NUMBERS[1:6], EMOJI["put_litter_in_its_place"], EMOJI["stop_button"]) + ) + await message.edit(embed=discord.Embed(title=title, description=description)) + + try: + reaction, user = await self.bot.wait_for( + "reaction_add", check=get_reaction_check(moderators=True), timeout=40 + ) + if left := await edit_custom_react(message, reaction, user, custom_react, proposals): + return left + except asyncio.TimeoutError: + pass + + await clear_options(message) + return False + + async def edit_custom_react( + message: discord.Message, + reaction: discord.Reaction, + user, + custom_react, + proposals, + ) -> bool: # Returns whether to leave the wizard + db: aiosqlite.Connection + + await clear_options(message) + + custom_react_id = custom_react[0] + delete = custom_react[4] + anywhere = custom_react[5] + dm = custom_react[6] + + noun = f"reaction{' proposal' if proposals else ''}" + noun_custom = f"custom {noun}" + + message_kept = f"Successfully kept current option! Returning to list of {noun}s..." + message_modified = f"Option successfully modified! Returning to list of {noun}s..." + message_time_out = f"The modification of the {noun_custom} timed out. Returning to list of {noun}s..." + + footer_modifying_stop = f"{user} is currently modifying a {noun_custom}. \nWrite '{STOP_TEXT}' to cancel." + footer_modified = f"Modified by {user}." + + async def _edit_reaction_and_rebuild(react_id, key, value): + db_: aiosqlite.Connection + async with self.db() as db_: + await db_.execute( + f"UPDATE CustomReactions SET {key} = ? WHERE CustomReactionID = ?", (value, react_id) + ) + await db_.commit() + await self.rebuild_lists() + + # Edit the prompt + if reaction.emoji == EMOJI["one"]: + await message.edit( + embed=( + discord.Embed( + title=f"Modify a {noun_custom}", description="Please enter the new prompt" + ).set_footer(text=footer_modifying_stop, icon_url=user.avatar_url) + ) + ) + + try: + msg = await self.bot.wait_for("message", check=get_msg_check(msg_user=user), timeout=60) + except asyncio.TimeoutError: + await message.edit(embed=discord.Embed(title=message_time_out)) + await asyncio.sleep(5) + return False + + prompt = msg.content + await msg.delete() + + if prompt.lower() == STOP_TEXT: + return await leave(message) + + await _edit_reaction_and_rebuild(custom_react_id, "Prompt", prompt) + + await message.edit( + embed=discord.Embed( + title=f"Prompt successfully modified! Returning to list of {noun}s..." + ).set_footer(text=footer_modified, icon_url=user.avatar_url) + ) + await asyncio.sleep(5) + + # Edit the response + elif reaction.emoji == EMOJI["two"]: + await message.edit( + embed=discord.Embed( + title=f"Modify a {noun_custom}", description="Please enter the new response" + ).set_footer(text=footer_modifying_stop, icon_url=user.avatar_url) + ) + + try: + msg = await self.bot.wait_for("message", check=get_msg_check(msg_user=user), timeout=60) + except asyncio.TimeoutError: + await message.edit(embed=discord.Embed(title=message_time_out)) + await asyncio.sleep(5) + return False + + response = msg.content + await msg.delete() + + if response.lower() == STOP_TEXT: + return await leave(message) + + await _edit_reaction_and_rebuild(custom_react_id, "Response", response) + + title = f"Response successfully modified! Returning to list of {noun}s..." + await message.edit( + embed=discord.Embed(title=title).set_footer(text=footer_modified, icon_url=user.avatar_url) + ) + await asyncio.sleep(5) + + # Edit the "delete" option + elif reaction.emoji == EMOJI["three"]: + await message.edit(embed=LOADING_EMBED) + description = ( + f"Should the message that calls the reaction be deleted?\n" + f"{EMOJI['zero']} No\n" + f"{EMOJI['one']} Yes" + ) + current_options.clear() + await message.clear_reactions() + current_options.extend((*NUMBERS[0:2], EMOJI["stop_button"])) + await add_yes_or_no_reactions(message) + await message.edit( + embed=( + discord.Embed( + title=f"Modify a {noun_custom}. React with the option you want", + description=description, + ).set_footer( + text=f"{user} is currently modifying a {noun_custom}. \n", + icon_url=user.avatar_url, + ) + ) + ) + + try: + reaction, reaction_user = await self.bot.wait_for( + "reaction_add", check=get_reaction_check(reaction_user=user), timeout=60 + ) + except asyncio.TimeoutError: + await message.edit(embed=discord.Embed(title=message_time_out)) + await asyncio.sleep(5) + current_options.clear() + await message.clear_reactions() + return False + + current_options.clear() + await message.clear_reactions() + + if reaction.emoji == EMOJI["stop_button"]: + return await leave(message) + + if reaction.emoji in (EMOJI["zero"], EMOJI["one"]): + # 0: Deactivate the "delete" option + # 1: Activate the "delete" option + new_value = int(reaction.emoji == EMOJI["one"]) # 1 if one, 0 if zero; simple as that + + if delete == new_value: + title = message_kept + else: + title = message_modified + await _edit_reaction_and_rebuild(custom_react_id, "DeletePrompt", new_value) + + await message.edit( + embed=discord.Embed(title=title).set_footer(text=footer_modified, icon_url=user.avatar_url) + ) + + await asyncio.sleep(5) + + # Edit the "anywhere" option + elif reaction.emoji == EMOJI["four"]: + await message.edit(embed=LOADING_EMBED) + title = f"Modify a {noun_custom}. React with the option you want" + footer = f"{user} is currently modifying a {noun_custom}. \n" + description = ( + f"Should the custom reaction be activated if the prompt is anywhere in a message?\n" + f"{EMOJI['zero']} No\n" + f"{EMOJI['one']} Yes" + ) + current_options.clear() + await message.clear_reactions() + current_options.extend((*NUMBERS[0:2], EMOJI["stop_button"])) + await add_yes_or_no_reactions(message) + await message.edit( + embed=discord.Embed(title=title, description=description).set_footer( + text=footer, icon_url=user.avatar_url + ) + ) + try: + reaction, reaction_user = await self.bot.wait_for( + "reaction_add", check=get_reaction_check(reaction_user=user), timeout=60 + ) + + except asyncio.TimeoutError: + title = f"The modification of the {noun_custom} timed out. Returning to list of {noun}s..." + await message.edit(embed=discord.Embed(title=title)) + await asyncio.sleep(5) + current_options.clear() + await message.clear_reactions() + return False + + current_options.clear() + await message.clear_reactions() + + if reaction.emoji == EMOJI["stop_button"]: + return await leave(message) + + if reaction.emoji in (EMOJI["zero"], EMOJI["one"]): + # 0: Deactivate the "anywhere" option + # 1: Activate the "anywhere" option + new_value = int(reaction.emoji == EMOJI["one"]) # 1 if one, 0 if zero; simple as that + + if anywhere == new_value: + title = message_kept + else: + title = message_modified + await _edit_reaction_and_rebuild(custom_react_id, "Anywhere", new_value) + + await message.edit( + embed=discord.Embed(title=title).set_footer(text=footer_modified, icon_url=user.avatar_url) + ) + + await asyncio.sleep(5) + + # Edit "dm" option + elif reaction.emoji == EMOJI["five"]: + await message.edit(embed=LOADING_EMBED) + title = f"Modify a {noun_custom}. React with the option you want" + footer = f"{user} is currently modifying a {noun_custom}. \n" + description = ( + f"Should the reaction be sent in the DMs of the user who called the reaction " + f"instead of the channel?\n" + f"{EMOJI['zero']} No\n" + f"{EMOJI['one']} Yes" + ) + current_options.clear() + await message.clear_reactions() + current_options.extend((*NUMBERS[0:2], EMOJI["stop_button"])) + await add_yes_or_no_reactions(message) + await message.edit( + embed=( + discord.Embed(title=title, description=description).set_footer( + text=footer, icon_url=user.avatar_url + ) + ) + ) + try: + reaction, reaction_user = await self.bot.wait_for( + "reaction_add", check=get_reaction_check(reaction_user=user), timeout=60 + ) + + except asyncio.TimeoutError: + title = f"The modification of the {noun_custom} timed out. Returning to list of {noun}s..." + await message.edit(embed=discord.Embed(title=title)) + await asyncio.sleep(5) + current_options.clear() + await message.clear_reactions() + return False + + current_options.clear() + await message.clear_reactions() + + if reaction.emoji == EMOJI["stop_button"]: + return await leave(message) + + if reaction.emoji in (EMOJI["zero"], EMOJI["one"]): + # 0: Deactivate the "dm" option + # 1: Activate the "dm" option + new_value = int(reaction.emoji == EMOJI["one"]) # 1 if one, 0 if zero; simple as that + + if dm == new_value: + title = message_kept + else: + title = message_modified + await _edit_reaction_and_rebuild(custom_react_id, "DM", new_value) + + await message.edit( + embed=discord.Embed(title=title).set_footer(text=footer_modified, icon_url=user.avatar_url) + ) + + await asyncio.sleep(5) + + # Approve a custom reaction proposal + elif reaction.emoji == EMOJI["white_check_mark"]: + await _edit_reaction_and_rebuild(custom_react_id, "Proposal", 0) + + await message.edit( + embed=discord.Embed( + title=( + "Custom reaction proposal successfully approved! " + "Returning to list of current reaction proposals..." + ) + ).set_footer(text=f"Approved by {user}.", icon_url=user.avatar_url) + ) + + await asyncio.sleep(5) + + # Delete a custom reaction or proposal + elif reaction.emoji == EMOJI["put_litter_in_its_place"] or reaction.emoji == EMOJI["x"]: + async with self.db() as db: + await db.execute("DELETE FROM CustomReactions WHERE CustomReactionID = ?", (custom_react_id,)) + await db.commit() + title = f"Custom {noun} successfully rejected! Returning to list of {noun}s..." + footer = f"{'Rejected' if proposals else 'Deleted'} by {user}." + await message.edit(embed=discord.Embed(title=title).set_footer(text=footer, icon_url=user.avatar_url)) + await self.rebuild_lists() + await asyncio.sleep(5) + + # Stop + elif reaction.emoji == EMOJI["stop_button"]: + return await leave(message) + + return False + + async def list_placeholders(message, **_kwargs) -> Literal[False]: + title = "The following placeholders can be used in prompts and responses:" + description = ( + "-%user%: the user who called the prompt (can only be used in a response)\n" + "-%channel%: the name of the channel where the prompt was called (can only be used in a response) \n" + "-%1%, %2%, etc. up to %9%: Groups. When a prompt uses this, anything will match. For " + 'example, the prompt "i %1% apples" will work for any message that starts with "i" and ends ' + 'with "apples", such as "i really like apples". Then, the words that match to this ' + "group can be used in the response. For example, keeping the same prompt and using the response " + '"i %1% pears" will send "i really like pears"\n' + "-%[]%: a comma-separated choice list. There are two uses for this. The first is that when it is " + "used in a prompt, the prompt will accept either of the choices. For example, the prompt " + '"%[hello, hi, hey]% world" will work if someone writes "hello world", "hi world" or ' + '"hey world". The second use is that when it is ' + "used in a response, a random choice will be chosen from the list. For example, the response " + '"i %[like, hate]% you" will either send "i like you" or "i hate you". All placeholders ' + "can be used in choice lists (including choice lists themselves). If a choice contains commas, " + 'it can be surrounded by "" to not be split into different choices' + ) + await message.edit(embed=discord.Embed(title=title, description=description)) + return False + + async def leave(message, **_kwargs) -> Literal[True]: + await message.delete() + return True + + initial_message = await ctx.send(embed=LOADING_EMBED) + is_mod = discord.utils.get(main_user.roles, name=self.bot.config.moderator_role) is not None + + return await create_assistant(initial_message, is_mod) + + +def setup(bot): + bot.add_cog(CustomReactions(bot)) diff --git a/cogs/games.py b/canary/cogs/games.py similarity index 86% rename from cogs/games.py rename to canary/cogs/games.py index 77a9cf49f..20fcb1cd0 100644 --- a/cogs/games.py +++ b/canary/cogs/games.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2019) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -17,27 +15,28 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -# discord-py requirements -import discord -from discord.ext import commands +import asyncio -# Other utilities -import re +import aiosqlite +import discord import os -import sqlite3 -from time import time import pickle import random -import asyncio -from typing import Optional +import re + +from discord.ext import commands +from time import time from functools import partial + +from ..bot import Canary +from .base_cog import CanaryCog from .utils.dice_roll import dice_roll from .utils.clamp_default import clamp_default from .utils.hangman import HangmanState from .currency import HANGMAN_REWARD ROLL_PATTERN = re.compile(r"^(\d*)d(\d*)([+-]?\d*)$") -CATEGORY_SYNONYMS = { +CATEGORY_SYNONYMS: dict[str, str] = { "movies": "movie", "kino": "movie", "elements": "element", @@ -46,17 +45,19 @@ } -class Games(commands.Cog): - def __init__(self, bot, hangman_tbl_name: str): - self.bot = bot - self.hm_cool_win: int = bot.config.games["hm_cool_win"] - self.hm_norm_win: int = bot.config.games["hm_norm_win"] - self.hm_timeout: int = bot.config.games["hm_timeout"] +class Games(CanaryCog): + def __init__(self, bot: Canary, hangman_tbl_name: str) -> None: + super().__init__(bot) + + self.hm_cool_win: int = bot.config.games.hm_cool_win + self.hm_norm_win: int = bot.config.games.hm_norm_win + self.hm_timeout: int = bot.config.games.hm_timeout self.hm_locks: dict[discord.TextChannel, asyncio.Lock] = dict() + with open(f"{os.getcwd()}/data/premade/{hangman_tbl_name}.obj", "rb") as hangman_pkl: - self.hangman_dict: dict[str, tuple[list[tuple[str, Optional[str]]], str]] = pickle.load(hangman_pkl) + self.hangman_dict: dict[str, tuple[list[tuple[str, str | None]], str]] = pickle.load(hangman_pkl) - def help_str(self): + def help_str(self) -> str: cat_list: str = ", ".join( f"`{hm_cat}` (length: {len(self.hangman_dict[hm_cat][0])})" for hm_cat in sorted(self.hangman_dict.keys()) ) @@ -69,16 +70,17 @@ def help_str(self): "resend this message by typing `?{hm|hangman} help`." ) - def hm_msg_check(self, hm_channel: discord.TextChannel, lowered: str, msg: discord.Message): + @staticmethod + def hm_msg_check(hm_channel: discord.TextChannel, lowered: str, msg: discord.Message): return msg.channel == hm_channel and ( (len(msg.content) == 1 and msg.content.isalpha()) or msg.content.lower() == lowered ) @commands.command(aliases=["hm"]) - async def hangman(self, ctx, command: Optional[str] = None): + async def hangman(self, ctx, command: str | None = None): """ play a nice game of hangman with internet strangers! - guesses must be single letters (interpreted in a case insensitive manner) or the entire correct word. + guesses must be single letters (interpreted in a case-insensitive manner) or the entire correct word. can either be called with "?{hm|hangman}" or "?{hm|hangman} x", where x is a valid category argument. see all categories by typing "?{hm|hangman} help". quit an ongoing game by typing "?{hm|hangman} quit". @@ -86,7 +88,7 @@ async def hangman(self, ctx, command: Optional[str] = None): rules: - 5 wrong guesses allowed - for a guess to be registered, it must either be the full correct word or a single letter - - guesses are interpreted in a case insensitive manner + - guesses are interpreted in a case-insensitive manner """ await ctx.trigger_typing() @@ -102,7 +104,8 @@ async def hangman(self, ctx, command: Optional[str] = None): await ctx.send("no game is currently being played in this channel") return - category: str = CATEGORY_SYNONYMS.get(command, command) or random.choice(list(self.hangman_dict.keys())) + command_str: str = command or "" + category: str = CATEGORY_SYNONYMS.get(command_str, command_str) or random.choice(list(self.hangman_dict.keys())) try: word_list, pretty_name = self.hangman_dict[category] except KeyError: @@ -110,9 +113,7 @@ async def hangman(self, ctx, command: Optional[str] = None): return if ctx.message.channel in self.hm_locks: - await ctx.send( - "command `hm|hangman` cannot " "be used to start a new game " "while one is already going on" - ) + await ctx.send("command `hm|hangman` cannot be used to start a new game while one is already going on") return channel_lock = asyncio.Lock() @@ -121,7 +122,7 @@ async def hangman(self, ctx, command: Optional[str] = None): game_state = HangmanState("[REDACTED]" if command is None else pretty_name, word_list) timeout_dict: dict[discord.Member, float] = {} - winner: Optional[discord.Member] = None + winner: discord.Member | None = None cool_win: bool = False msg_check = partial(self.hm_msg_check, ctx.message.channel, game_state.lword) @@ -129,7 +130,6 @@ async def hangman(self, ctx, command: Optional[str] = None): await ctx.send(embed=game_state.embed) while True: - msg_task = asyncio.create_task(self.bot.wait_for("message", check=msg_check, timeout=self.hm_timeout)) quit_task = asyncio.create_task(channel_lock.acquire()) done, _ = await asyncio.wait([msg_task, quit_task], return_when=asyncio.FIRST_COMPLETED) @@ -198,7 +198,7 @@ async def hangman(self, ctx, command: Optional[str] = None): game_state.embed.set_image(url=game_state.img) await ctx.send(embed=game_state.embed) await ctx.send( - f"congratulations `{winner}`, you solved the hangman, " f"earning you {self.hm_norm_win} cheeps" + f"congratulations `{winner}`, you solved the hangman, earning you {self.hm_norm_win} cheeps" ) break else: @@ -222,16 +222,16 @@ async def hangman(self, ctx, command: Optional[str] = None): del self.hm_locks[ctx.message.channel] if winner is not None: - conn = sqlite3.connect(self.bot.config.db_path) - await self.bot.get_cog("Currency").create_bank_transaction( - conn.cursor(), - winner, - self.hm_cool_win if cool_win else self.hm_norm_win, - HANGMAN_REWARD, - {"cool": cool_win}, - ) - conn.commit() - conn.close() + db: aiosqlite.Connection + async with self.db() as db: + await self.bot.get_cog("Currency").create_bank_transaction( + db, + winner, + self.hm_cool_win if cool_win else self.hm_norm_win, + HANGMAN_REWARD, + {"cool": cool_win}, + ) + await db.commit() @commands.command() async def roll(self, ctx, arg: str = "", mpr: str = ""): diff --git a/cogs/helpers.py b/canary/cogs/helpers.py similarity index 77% rename from cogs/helpers.py rename to canary/cogs/helpers.py index 1a42e6275..692fd418b 100644 --- a/cogs/helpers.py +++ b/canary/cogs/helpers.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with Canary. If not, see . - # discord-py requirements import discord from discord.ext import commands @@ -29,20 +28,21 @@ import cv2 import numpy as np import googletrans -import os # Other utilities -import re -import math -import time +import aiosqlite import datetime +import math import random +import re +import time + +from .base_cog import CanaryCog +from .utils.arg_converter import ArgConverter, StrConverter from .utils.paginator import Pages from .utils.custom_requests import fetch from .utils.site_save import site_save -from .utils.checks import is_moderator, is_developer -import sqlite3 -from .utils.arg_converter import ArgConverter, StrConverter +from .utils.checks import is_developer from discord.ext.commands import MessageConverter MCGILL_EXAM_URL = "https://www.mcgill.ca/exams/dates" @@ -51,8 +51,6 @@ MCGILL_KEY_DATES_URL = "https://www.mcgill.ca/importantdates/key-dates" -WTTR_IN_MOON_URL = "http://wttr.in/moon.png" - URBAN_DICT_TEMPLATE = "http://api.urbandictionary.com/v0/define?term={}" LMGTFY_TEMPLATE = "https://letmegooglethat.com/?q={}" @@ -70,24 +68,22 @@ MAIN_WEBHOOKS_PREFIX = "Main webhook for #" -class Helpers(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.guild: discord.Guild | None = None - - @commands.Cog.listener() - async def on_ready(self): - self.guild = self.bot.get_guild(self.bot.config.server_id) - +class Helpers(CanaryCog): @commands.command(aliases=["exams"]) - async def exam(self, ctx): + async def exam(self, ctx: commands.Context): """Retrieves the exam schedule link from McGill's Exam website.""" await ctx.trigger_typing() r = await fetch(MCGILL_EXAM_URL, "content") - soup = BeautifulSoup(r, "html.parser") - link = soup.find("a", href=re.compile("exams/files/exams"))["href"] + soup = BeautifulSoup(r, "lxml") + + if (link_el := soup.find("a", href=re.compile(r"exams/files/exams"))) is None: + return await ctx.send("Error: could not find exam link on McGill's website") + + link = link_el["href"] + if isinstance(link, list): # Attribute access can return str or list[str] + link = link[0] if link[:2] == "//": link = "https:" + link @@ -132,42 +128,69 @@ def _calculate_feels_like(temp: float, humidity: float, ws_kph: float) -> str: return f"{round(feels_like, 1)}°C" @commands.command() - @site_save("http://weather.gc.ca/city/pages/qc-147_metric_e.html") - async def weather(self, ctx): + @site_save("https://dd.weather.gc.ca/citypage_weather/xml/QC/s0000635_e.xml") + async def weather(self, ctx: commands.Context): """ Retrieves current weather conditions. - Data taken from http://weather.gc.ca/city/pages/qc-147_metric_e.html + Data taken from https://dd.weather.gc.ca/citypage_weather/xml/QC/s0000635_e.xml """ await ctx.trigger_typing() r = await fetch(self.bot.config.gc_weather_url, "content") - soup = BeautifulSoup(r, "html.parser") + soup = BeautifulSoup(r, features="xml") - def retrieve_string(label): - if elem := soup.find("dt", string=label).find_next_sibling(): + # We only care about the current conditions, rest can be discarded + soup = soup.currentConditions + + # Getting the wind specifically, because otherwise it starts being ugly very quickly + wind = soup.find("wind") + + def retrieve_string(label, search=None, search_soup=soup): + if elem := search_soup.find(label, string=search): return elem.get_text().strip() return None - observed_string = retrieve_string("Date: ") - temperature_string = retrieve_string("Temperature:") - condition_string = retrieve_string("Condition:") - pressure_string = retrieve_string("Pressure:") - tendency_string = retrieve_string("Tendency:") - wind_string = retrieve_string("Wind:") - humidity_string = retrieve_string("Humidity:") + def retrieve_attribute(label, key, search_soup=soup): + if attr := search_soup.find(label)[key]: + return attr.strip() + return None + + observed_string = retrieve_string("textSummary", re.compile("(EST|EDT)")) + temperature_string = retrieve_string("temperature") + "°C" + condition_string = retrieve_string("condition") + pressure_string = retrieve_string("pressure") + " kPa" + tendency_string = retrieve_attribute("pressure", "tendency") + wind_string = ( + retrieve_string("direction", search_soup=wind) + + " " + + retrieve_string("speed", search_soup=wind) + + " " + + retrieve_attribute("speed", "units", wind) + ) + humidity_string = retrieve_string("relativeHumidity") + + feels_like_values = { + "temp": re.search(r"-?\d+\.\d", temperature_string), + "humidity": re.search(r"\d+", humidity_string), + "ws_kph": re.search(r"\d+", wind_string), + } + feels_like_string = ( - Helpers._calculate_feels_like( - temp=float(re.search(r"-?\d+\.\d", temperature_string).group()), - humidity=float(re.search(r"\d+", humidity_string).group()), - ws_kph=float(re.search(r"\d+", wind_string).group()), + Helpers._calculate_feels_like(**{k: float(v.group()) for k, v in feels_like_values.items() if v}) + if all( + ( + humidity_string, + temperature_string, + wind_string, + *feels_like_values.values(), + ) ) - if humidity_string and temperature_string and wind_string else "n/a" ) weather_now = discord.Embed( title="Current Weather", - description=f"Conditions observed at {observed_string or '[REDACTED]'}", + description=f"Conditions observed on {observed_string or '[REDACTED]'}", colour=0x7EC0EE, ) weather_now.add_field(name="Temperature", value=temperature_string or "n/a", inline=True) @@ -177,58 +200,61 @@ def retrieve_string(label): weather_now.add_field(name="Wind Speed", value=wind_string or "n/a", inline=True) weather_now.add_field(name="Feels like", value=feels_like_string, inline=True) + # Send weather first, then figure out alerts + await ctx.send(embed=weather_now) + # Weather alerts + def no_alerts_embed(title: str | None = None): + return discord.Embed(title=title, description="No alerts in effect.", colour=0xFF0000) + r_alert = await fetch(self.bot.config.gc_weather_alert_url, "content") - alert_soup = BeautifulSoup(r_alert, "html.parser") + alert_soup = BeautifulSoup(r_alert, "lxml") alert_title = alert_soup.find("h1", string=ALERT_REGEX) + + if not alert_title: + return await ctx.send(embed=no_alerts_embed()) + alert_title_text = alert_title.get_text().strip() + no_alerts = no_alerts_embed(alert_title_text) # Only gets first

of warning. Subsequent paragraphs are ignored. - try: - alert_category = alert_title.find_next("h2") - alert_date = alert_category.find_next("span") - alert_heading = alert_date.find_next("strong") - # This is a string for some reason. - alert_location = alert_heading.find_next(string=MTL_REGEX) - # Only gets first

of warning. Subsequent paragraphs are ignored - alert_content = ". ".join(alert_location.find_next("p").get_text().strip().split(".")).rstrip() - - weather_alert = discord.Embed( - title=alert_title_text, - description="**{}** at {}".format(alert_category.get_text().strip(), alert_date.get_text().strip()), - colour=0xFF0000, - ) - weather_alert.add_field( - name=alert_heading.get_text().strip(), - value=f"**{alert_location.strip()}**\n{alert_content}", - inline=True, - ) - - except AttributeError: - weather_alert = discord.Embed(title=alert_title_text, description="No alerts in effect.", colour=0xFF0000) - # TODO Finish final message. Test on no-alert condition. + if (alert_category := alert_title.find_next("h2")) is None: + return await ctx.send(embed=no_alerts) + if (alert_date := alert_category.find_next("span")) is None: + return await ctx.send(embed=no_alerts) + if (alert_heading := alert_date.find_next("strong")) is None: + return await ctx.send(embed=no_alerts) + # This is a string for some reason. + if (alert_location := alert_heading.find_next(string=MTL_REGEX)) is None: + return await ctx.send(embed=no_alerts) + + # Only gets first

of warning. Subsequent paragraphs are ignored + if (alert_paragraph := alert_location.find_next("p")) is None: + return await ctx.send(embed=no_alerts) + + alert_content = ". ".join(alert_paragraph.get_text().strip().split(".")).rstrip() + + weather_alert = discord.Embed( + title=alert_title_text, + description=f"**{alert_category.get_text().strip()}** at {alert_date.get_text().strip()}", + colour=0xFF0000, + ) + weather_alert.add_field( + name=alert_heading.get_text().strip(), + value=f"**{alert_location.strip()}**\n{alert_content}", + inline=True, + ) # Sending final message - await ctx.send(embed=weather_now) await ctx.send(embed=weather_alert) @commands.command() - async def wttr(self, ctx): - """Retrieves Montreal's weather forecast from wttr.in""" - await ctx.send(self.bot.config.wttr_in_tpl.format(round(time.time()))) - - @commands.command(aliases=["wttrmoon"]) - async def wttr_moon(self, ctx): - """Retrieves the current moon phase from wttr.in/moon""" - await ctx.send(WTTR_IN_MOON_URL) - - @commands.command() - async def course(self, ctx, *, query: str): + async def course(self, ctx: commands.Context, *, query: str): """Prints a summary of the queried course, taken from the course - calendar. ie. ?course comp 206 + calendar. i.e. ?course comp 206 Note: Bullet points without colons (':') are not parsed because I have yet to see one that actually has useful information. """ @@ -238,13 +264,13 @@ async def course(self, ctx, *, query: str): # cases. Courses across multiple semesters have a suffix like D1/D2. result = re.compile(r"([A-Za-z]{3}[A-Za-z0-9])\s*(\d{3}\s*(\w\d)?)", re.IGNORECASE | re.DOTALL).search(query) if not result: - await ctx.send(":warning: Incorrect format. The correct format is `?course " "`.") + await ctx.send(":warning: Incorrect format. The correct format is `?course `.") return search_term = re.sub(r"\s+", "", f"{result.group(1)}-{result.group(2)}") - url = self.bot.config.course_tpl.format(search_term) + url = self.bot.config.course_tpl.format(self.bot.config.course_year_range, search_term) r = await fetch(url, "content") - soup = BeautifulSoup(r, "html.parser") + soup = BeautifulSoup(r, "lxml") # TODO: brute-force parsing at the moment title = soup.find_all("h1", {"id": "page-title"})[0].get_text().strip() @@ -265,22 +291,30 @@ async def course(self, ctx, *, query: str): (a, b) = i.get_text().split(":", 1) tidbits.append((a.strip(), b.strip())) - em = discord.Embed(title=title, description=url, colour=0xDA291C) - em.add_field(name="Overview", value=overview, inline=False) - em.add_field(name="Terms", value=terms, inline=False) - em.add_field(name="Instructor(s)", value=instructors, inline=False) - for (a, b) in tidbits: + em = ( + discord.Embed(title=title, description=url, colour=0xDA291C) + .add_field(name="Overview", value=overview, inline=False) + .add_field(name="Terms", value=terms, inline=False) + .add_field(name="Instructor(s)", value=instructors, inline=False) + ) + for a, b in tidbits: em.add_field(name=a, value=b, inline=False) await ctx.send(embed=em) @commands.command() - async def keydates(self, ctx): + async def keydates(self, ctx: commands.Context): """Retrieves the important dates for the current term (Winter from January-April, Fall from May-December).""" await ctx.trigger_typing() - soup = BeautifulSoup(await fetch(MCGILL_KEY_DATES_URL, "content"), "html.parser") + try: + content = await fetch(MCGILL_KEY_DATES_URL, "content") + except Exception as e: + await ctx.send("Encountered an error while contacting the McGill server.") + raise e + + soup = BeautifulSoup(content, "lxml") now = datetime.datetime.now() current_year, current_month = now.year, now.month @@ -310,7 +344,7 @@ async def keydates(self, ctx): if node.name == "ul": sections.append(node.get_text()) previous = node.previous_sibling.previous_sibling - if previous.name == "p": + if previous and previous.name == "p": headers.append(previous.get_text()) else: # just in case the layout changes again, at least the whole thing won't break @@ -334,7 +368,7 @@ async def keydates(self, ctx): await ctx.send(embed=em) @commands.command() - async def urban(self, ctx, *, query): + async def urban(self, ctx: commands.Context, *, query: str): """Fetches the top definitions from Urban Dictionary""" await ctx.trigger_typing() @@ -344,7 +378,7 @@ async def urban(self, ctx, *, query): definitions = definitions["list"][:5] if not definitions: - await ctx.send("No definition found for **%s**." % query) + await ctx.send(f"No definition found for **{query}**.") return markdown_url = f"[{definitions[0]['word']}]({url})" @@ -360,7 +394,7 @@ async def urban(self, ctx, *, query): p = Pages( ctx, item_list=definitions_list_text, - title="Definitions for '{}' from Urban Dictionary:".format(query), + title=f"Definitions for '{query}' from Urban Dictionary:", display_option=(3, 1), editable_content=False, ) @@ -368,13 +402,13 @@ async def urban(self, ctx, *, query): await p.paginate() @commands.command() - async def lmgtfy(self, ctx, *, query): + async def lmgtfy(self, ctx: commands.Context, *, query: str): """Generates a Let Me Google that For You link.""" url = LMGTFY_TEMPLATE.format(query.replace("+", "%2B").replace(" ", "+")) await ctx.send(url) @commands.command() - async def tex(self, ctx, *, query: str): + async def tex(self, ctx: commands.Context, *, query: str): """Parses and prints LaTeX equations.""" await ctx.trigger_typing() @@ -394,15 +428,21 @@ async def tex(self, ctx, *, query: str): tex += "\\[" + sp[2 * i + 1] + "\\]" buf = BytesIO() - preview( - tex, - preamble=LATEX_PREAMBLE, - viewer="BytesIO", - outputbuffer=buf, - euler=False, - dvioptions=["-T", "tight", "-z", "9", "--truecolor", "-D", "600"], - ) - buf.seek(0) + + try: + preview( + tex, + preamble=LATEX_PREAMBLE, + viewer="BytesIO", + outputbuffer=buf, + euler=False, + dvioptions=["-T", "tight", "-z", "9", "--truecolor", "-D", "600"], + ) + buf.seek(0) + except RuntimeError as e: + await ctx.send("Encountered an error while trying to render TeX.") + raise e + img_bytes = np.asarray(bytearray(buf.read()), dtype=np.uint8) img = cv2.imdecode(img_bytes, cv2.IMREAD_UNCHANGED) img2 = cv2.copyMakeBorder(img, 115, 115, 115, 115, cv2.BORDER_CONSTANT, value=(255, 255, 255)) @@ -413,32 +453,34 @@ async def tex(self, ctx, *, query: str): await ctx.send(file=discord.File(fp=img_bytes, filename=fn)) @commands.command() - async def search(self, ctx, *, query: str): + async def search(self, ctx: commands.Context, *, query: str): """Shows results for the queried keyword(s) in McGill courses""" keyword = query.replace(" ", "+") pagelimit = 5 pagenum = 0 - courses = [] + courses: list = [] await ctx.trigger_typing() while pagenum < pagelimit: - r = await fetch(self.bot.config.course_search_tpl.format(keyword, pagenum), "content") - soup = BeautifulSoup(r, "html.parser") + r = await fetch( + self.bot.config.course_search_tpl.format(self.bot.config.course_year_range, keyword, pagenum), "content" + ) + soup = BeautifulSoup(r, "lxml") found = soup.find_all("div", {"class": "views-row"}) if len(found) < 1: break - else: - courses = courses + found - pagenum += 1 + + courses = courses + found + pagenum += 1 if len(courses) < 1: await ctx.send("No course found for: {}.".format(query)) return - course_list = {"names": [], "values": []} + course_list: dict[str, list[str]] = {"names": [], "values": []} for course in courses: # split results into titles + information title = course.find_all("h4")[0].get_text().split(" ") @@ -448,14 +490,14 @@ async def search(self, ctx, *, query: str): p = Pages( ctx, item_list=course_list, - title="Courses found for {}".format(query), + title=f"Courses found for {query}", display_option=(2, 10), editable_content=False, ) await p.paginate() @commands.command() - async def mose(self, ctx, dollar: float): + async def mose(self, ctx: commands.Context, dollar: float): """Currency conversion. Converts $$$ to the equivalent number of samosas, based on holy prices. Usage: `?mose ` @@ -468,23 +510,23 @@ async def mose(self, ctx, dollar: float): await ctx.send("${:.2f} is worth {} samosas.".format(dollar, total)) @commands.command() - async def tepid(self, ctx): + async def tepid(self, ctx: commands.Context): """Retrieves the CTF printers' statuses from tepid.science.mcgill.ca""" data = await fetch(self.bot.config.tepid_url, "json") for key, value in data.items(): await ctx.send(f"At least one printer in {key} is up!" if value else f"Both printers in {key} are down.") @commands.command() - async def modpow(self, ctx, a, b, m): + async def modpow(self, ctx: commands.Context, a, b, m): """Calculates a^b mod m, where a, b, c are big integers""" try: a, b, m = map(int, (a, b, m)) await ctx.send(pow(a, b, m)) except ValueError: - ctx.send("Input must be integers") + await ctx.send("Input must be integers") @commands.command(aliases=["foodspot", "fs", "food", "foodspotting", "food_spotting"]) - async def food_spot(self, ctx, *args): + async def food_spot(self, ctx: commands.Context, *args): # Written by @le-potate """Posts a food sale in #foodspotting. Use: `?foodspot Samosas in leacock` @@ -499,7 +541,7 @@ async def food_spot(self, ctx, *args): ) embed = discord.Embed(title="Food spotted", description=" ".join(args) if args else "\u200b") embed.set_footer( - text=("Added by {0} • Use '{1}foodspot' or '{1}fs' if you spot " "food (See '{1}help foodspot')").format( + text=("Added by {0} • Use '{1}foodspot' or '{1}fs' if you spot food (See '{1}help foodspot')").format( ctx.message.author, self.bot.config.command_prefix[0] ), icon_url=ctx.message.author.avatar_url, @@ -513,7 +555,7 @@ async def food_spot(self, ctx, *args): await channel.send(embed=embed) @commands.command() - async def choose(self, ctx, *, input_opts: str): + async def choose(self, ctx: commands.Context, *, input_opts: str): """ Randomly chooses one of the given options delimited by semicola or commas. @@ -525,7 +567,7 @@ async def choose(self, ctx, *, input_opts: str): await ctx.send(embed=embed) @commands.command(aliases=["color"]) - async def colour(self, ctx, *, arg: str): + async def colour(self, ctx: commands.Context, *, arg: str): """Shows a small image filled with the given hex colour. Usage: `?colour hex` """ @@ -551,7 +593,7 @@ async def colour(self, ctx, *, arg: str): await ctx.send(file=discord.File(fp=buffer, filename=fn)) @commands.command(aliases=["ui", "av", "avi", "userinfo"]) - async def user_info(self, ctx, user: discord.Member = None): + async def user_info(self, ctx: commands.Context, user: discord.Member | None = None): """ Show user info and avatar. Displays the information of the user @@ -576,7 +618,7 @@ async def user_info(self, ctx, user: discord.Member = None): await ctx.send(embed=ui_embed) @commands.command(aliases=["trans"]) - async def translate(self, ctx, command: str, *, inp_str: str = None): + async def translate(self, ctx: commands.Context, command: str, *, inp_str: str | None = None): """ Command used to translate some text from one language to another Takes two arguments: the source/target languages and the text to translate @@ -624,22 +666,19 @@ async def translate(self, ctx, command: str, *, inp_str: str = None): # Validation of language codes codes = command.replace("_", "-").split(">") if len(codes) != 2: - await ctx.send(f"Argument `{command}` is not properly formatted. " f"See `?translate help` to learn more.") + await ctx.send(f"Argument `{command}` is not properly formatted. See `?translate help` to learn more.") return source = codes[0].lower().strip() translator = googletrans.Translator() - detection = None if source == "": detection = translator.detect(inp_str) source = detection.lang elif source not in googletrans.LANGUAGES: - await ctx.send(f"`{source}` is not a valid language code. " f"See `?translate codes` for language codes.") + await ctx.send(f"`{source}` is not a valid language code. See `?translate codes` for language codes.") return destination = codes[1].lower().strip() if destination not in googletrans.LANGUAGES: - await ctx.send( - f"`{destination}` is not a valid language code. " f"See `?translate codes` for language codes." - ) + await ctx.send(f"`{destination}` is not a valid language code. See `?translate codes` for language codes.") return await ctx.send( @@ -652,10 +691,13 @@ async def translate(self, ctx, command: str, *, inp_str: str = None): @commands.max_concurrency(1, per=commands.BucketType.user, wait=False) @commands.command() @is_developer() - async def create_main_webhooks(self, ctx): + async def create_main_webhooks(self, ctx: commands.Context): """ - Create the general-use webhooks for each channel. Ignores channels that already have them + Create the general-use webhooks for each channel. The command ignores channels that already have webhooks. """ + if not self.guild: + return + ignored = 0 for channel in self.guild.text_channels: try: @@ -673,12 +715,18 @@ async def create_main_webhooks(self, ctx): await ctx.send(f"Created webhook for {channel.mention}") except discord.errors.Forbidden: ignored += 1 + await ctx.send(f"Ignored {ignored} channel{'s' if ignored == 1 else ''} without bot permissions") await ctx.send("Job completed.") - async def spoilerize_utility(self, ctx, message, reason: str = None, moderator: bool = False): + async def spoilerize_utility( + self, ctx: commands.Context, message: discord.Message, reason: str | None = None, moderator: bool = False + ) -> None: + db: aiosqlite.Connection + if not moderator and ctx.author != message.author: return + # get the webhook for this channel webhook = discord.utils.find( lambda w: w.user == self.bot.user and w.name[: len(MAIN_WEBHOOKS_PREFIX)] == MAIN_WEBHOOKS_PREFIX, @@ -729,37 +777,39 @@ async def spoilerize_utility(self, ctx, message, reason: str = None, moderator: await webhook.send(files=files, username=author.display_name, avatar_url=author.avatar_url, wait=True) ) - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - for spoilerized_message in spoilerized_messages: - c.execute("REPLACE INTO SpoilerizedMessages VALUES (?, ?)", (spoilerized_message.id, author.id)) - conn.commit() - conn.close() + async with self.db() as db: + for spoilerized_message in spoilerized_messages: + await db.execute("REPLACE INTO SpoilerizedMessages VALUES (?, ?)", (spoilerized_message.id, author.id)) + await db.commit() # delete original message await message.delete() @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): + async def on_raw_reaction_add(self, payload) -> None: + db: aiosqlite.Connection + + if not self.guild: + return + if payload.guild_id != self.guild.id or str(payload.emoji) != "🚮": return + # if the put_litter_in_its_place react was used check if it was # on a spoilerized message by its original author, and if so delete it - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute( + + found = await self.fetch_one( "SELECT * From SpoilerizedMessages WHERE MessageID=? AND UserID=?", (int(payload.message_id), int(payload.member.id)), ) - found = c.fetchone() + if found: channel = utils.get(self.guild.text_channels, id=payload.channel_id) message = await channel.fetch_message(payload.message_id) await message.delete() - conn.close() @commands.command(alias=["spoiler"]) - async def spoilerize(self, ctx, *args): + async def spoilerize(self, ctx: commands.Context, *args): """ Spoilerize a message or a range of messages (max 30 messages) @@ -832,13 +882,15 @@ async def spoilerize(self, ctx, *args): if limit_reached: await ctx.send( - "Could not spoilerize all messages: Max 30 at the same time (this message will be deleted in 15 seconds).", + "Could not spoilerize all messages: Max 30 at the same time (this message will be deleted in 15 " + "seconds).", delete_after=15, ) else: if moderator: await ctx.send( - f"Completed spoilerization of {len(messages)} messages (this message will be deleted in 15 seconds).", + f"Completed spoilerization of {len(messages)} messages (this message will be deleted in 15 " + f"seconds).", delete_after=15, ) else: diff --git a/cogs/images.py b/canary/cogs/images.py similarity index 71% rename from cogs/images.py rename to canary/cogs/images.py index dda476967..037cf6806 100644 --- a/cogs/images.py +++ b/canary/cogs/images.py @@ -15,85 +15,79 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -# imports for Discord from discord.ext import commands -# misc imports -import os +from ..bot import Canary +from .base_cog import CanaryCog from .utils import image_helpers as ih -class Images(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.max_size = self.bot.config.images["max_image_size"] - self.hist_lim = self.bot.config.images["image_history_limit"] - self.max_rad = self.bot.config.images["max_radius"] - self.max_itr = self.bot.config.images["max_iterations"] - - @commands.Cog.listener() - async def on_ready(self): - if not os.path.exists("./tmp/"): - os.mkdir("./tmp/", mode=0o755) +class Images(CanaryCog): + def __init__(self, bot: Canary): + super().__init__(bot) + self.max_size: int = self.bot.config.images.max_image_size + self.hist_lim: int = self.bot.config.images.image_history_limit + self.max_rad: int = self.bot.config.images.max_radius + self.max_itr: int = self.bot.config.images.max_iterations @commands.command() - async def polar(self, ctx): + async def polar(self, ctx: commands.Context): """ Transform Cartesian to polar coordinates. """ await ih.filter_image(self.bot.loop, ih.polar, ctx, self.hist_lim, self.max_size) @commands.command() - async def cart(self, ctx): + async def cart(self, ctx: commands.Context): """ Transform from polar to Cartesian coordinates. """ await ih.filter_image(self.bot.loop, ih.cart, ctx, self.hist_lim, self.max_size) @commands.command() - async def blur(self, ctx, iterations: int = 1): + async def blur(self, ctx: commands.Context, iterations: int = 1): """ Blur the image """ await ih.filter_image(self.bot.loop, ih.blur, ctx, self.hist_lim, self.max_size, iterations, self.max_itr) @commands.command(aliases=["left", "right"]) - async def hblur(self, ctx, radius: int = 10): + async def hblur(self, ctx: commands.Context, radius: int = 10): """ Blur the image horizontally """ await ih.filter_image(self.bot.loop, ih.hblur, ctx, self.hist_lim, self.max_size, radius, self.max_rad) @commands.command(aliases=["up", "down"]) - async def vblur(self, ctx, radius: int = 10): + async def vblur(self, ctx: commands.Context, radius: int = 10): """ Blur the image vertically """ await ih.filter_image(self.bot.loop, ih.vblur, ctx, self.hist_lim, self.max_size, radius, self.max_rad) @commands.command(aliases=["zoom", "radial"]) - async def rblur(self, ctx, radius: int = 10): + async def rblur(self, ctx: commands.Context, radius: int = 10): """ Radial blur """ await ih.filter_image(self.bot.loop, ih.rblur, ctx, self.hist_lim, self.max_size, radius, self.max_rad) @commands.command(aliases=["circle", "circular", "spin"]) - async def cblur(self, ctx, radius: int = 10): + async def cblur(self, ctx: commands.Context, radius: int = 10): """ Circular blur """ await ih.filter_image(self.bot.loop, ih.cblur, ctx, self.hist_lim, self.max_size, radius, self.max_rad) @commands.command(aliases=["df", "dfry", "fry"]) - async def deepfry(self, ctx, iterations: int = 1): + async def deepfry(self, ctx: commands.Context, iterations: int = 1): """ - Deep fry an image, mhmm + Deep-fry an image, mhmm """ await ih.filter_image(self.bot.loop, ih.deepfry, ctx, self.hist_lim, self.max_size, iterations, self.max_itr) @commands.command() - async def noise(self, ctx, iterations: int = 1): + async def noise(self, ctx: commands.Context, iterations: int = 1): """ Add some noise to tha image!! """ diff --git a/cogs/info.py b/canary/cogs/info.py similarity index 88% rename from cogs/info.py rename to canary/cogs/info.py index d8aa32ff0..71af932d3 100644 --- a/cogs/info.py +++ b/canary/cogs/info.py @@ -15,25 +15,22 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -# discord-py requirements -import discord import subprocess from discord.ext import commands +from .base_cog import CanaryCog -class Info(commands.Cog): - def __init__(self, bot): - self.bot = bot - +class Info(CanaryCog): @commands.command() async def version(self, ctx): + # TODO: use asyncio.create_subprocess_shell version = subprocess.check_output(("git", "describe", "--tags"), universal_newlines=True).strip() commit, authored = ( subprocess.check_output(("git", "log", "-1", "--pretty=format:%h %aI"), universal_newlines=True) .strip() .split(" ") ) - await ctx.send(f"Version: `{version}`\nCommit: `{commit}` " f"authored `{authored}`") + await ctx.send(f"Version: `{version}`\nCommit: `{commit}` authored `{authored}`") def setup(bot): diff --git a/cogs/memes.py b/canary/cogs/memes.py similarity index 80% rename from cogs/memes.py rename to canary/cogs/memes.py index 8655bfe04..8a9cc70f5 100644 --- a/cogs/memes.py +++ b/canary/cogs/memes.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2021) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -20,17 +20,15 @@ import discord # Other utilities -import random import aiohttp +import random +from .base_cog import CanaryCog from .utils.auto_incorrect import auto_incorrect -class Memes(commands.Cog): - def __init__(self, bot): - self.bot = bot - +class Memes(CanaryCog): @commands.command() - async def bac(self, ctx, *, input_str: str = None): + async def bac(self, ctx: commands.Context, *, input_str: str = ""): """ Purposefully auto-incorrects inputted sentences Inputted text is either the content of the message to @@ -40,7 +38,7 @@ async def bac(self, ctx, *, input_str: str = None): well. Invoking message will be deleted. """ replying: bool = ctx.message.reference and ctx.message.reference.resolved - if input_str is None: + if input_str == "": if not replying: return input_str = ctx.message.reference.resolved.content @@ -61,7 +59,7 @@ async def bac(self, ctx, *, input_str: str = None): await ctx.message.delete() @commands.command() - async def lenny(self, ctx): + async def lenny(self, ctx: commands.Context): """ Lenny face """ @@ -69,38 +67,29 @@ async def lenny(self, ctx): await ctx.message.delete() @commands.command() - async def license(self, ctx): + async def license(self, ctx: commands.Context): """ License """ await ctx.send( - "This bot is free software: you can redistribute" - " it and/or modify it under the terms of the GNU" - " General Public License as published by the " - "Free Software Foundation, either version 3 of " - "the License, or (at your option) any later " - "version. **This bot is distributed in the hope " - "that it will be useful**, but WITHOUT ANY " - "WARRANTY; without even the implied warranty of " - "MERCHANTABILITY or **FITNESS FOR A PARTICULAR " - "PURPOSE**. See the GNU General Public License " - "for more details. This bot is developed " - "primarily by student volunteers with better " - "things to do. A copy of the GNU General Public " - "License is provided in the LICENSE.txt file " - "along with this bot. The GNU General Public " - "License can also be found at " - "." + "This bot is free software: you can redistribute it and/or modify it under the terms of the GNU" + " General Public License as published by the Free Software Foundation, either version 3 of " + "the License, or (at your option) any later version. **This bot is distributed in the hope " + "that it will be useful**, but WITHOUT ANY WARRANTY; without even the implied warranty of " + "MERCHANTABILITY or **FITNESS FOR A PARTICULAR PURPOSE**. See the GNU General Public License " + "for more details. This bot is developed primarily by student volunteers with better " + "things to do. A copy of the GNU General Public License is provided in the LICENSE.txt file " + "along with this bot. The GNU General Public License can also be found at ." ) await ctx.message.delete() @commands.command() - async def cheep(self, ctx): + async def cheep(self, ctx: commands.Context): """:^)""" await ctx.send("CHEEP CHEEP") @commands.command() - async def mix(self, ctx, *, input_str: str = None): + async def mix(self, ctx: commands.Context, *, input_str: str = ""): """Alternates upper/lower case for input string. Inputted text is either the content of the message to after the command or the content of the message to which @@ -108,12 +97,15 @@ async def mix(self, ctx, *, input_str: str = None): is replying a message, the bot will reply to that message as well. Invoking message will be deleted. """ + replying: bool = ctx.message.reference and ctx.message.reference.resolved - if input_str is None: + if input_str == "": if not replying: return input_str = ctx.message.reference.resolved.content + msg = "".join((c.upper() if random.randint(0, 1) else c.lower()) for c in input_str) + self.bot.mod_logger.info( f"?mix invoked: Author: '{ctx.message.author}', " f"Message: '{ctx.message.content}'" @@ -126,18 +118,19 @@ async def mix(self, ctx, *, input_str: str = None): else "" ) ) + await ctx.send(msg, reference=ctx.message.reference, mention_author=False) await ctx.message.delete() @commands.command(aliases=["boot"]) - async def pyramid(self, ctx, num: int = 2, emoji: str = "👢"): + async def pyramid(self, ctx: commands.Context, num: int = 2, emoji: str = "👢"): """ Draws a pyramid of boots, default is 2 unless user specifies an integer number of levels of boots between -8 and 8. Also accepts any other emoji, word or multiword (in quotes) string. """ - def pyramidy(n, m): + def pyramidy(n: int, m: int) -> str: # Limit emoji/string to 8 characters or Discord/potate mald return f"{' ' * ((m - n) * 3)}{(emoji[:8] + ' ') * n}" @@ -149,7 +142,7 @@ def pyramidy(n, m): await ctx.send(f"**\n{msg}**") @commands.command() - async def xkcd(self, ctx, command: str = None): + async def xkcd(self, ctx: commands.Context, command: str = ""): """ Enjoy a nice xkcd comic with some strangers on the internet! If no issue number is passed, returns a random xkcd. @@ -160,7 +153,7 @@ async def xkcd(self, ctx, command: str = None): await ctx.trigger_typing() async with aiohttp.ClientSession() as session: - if command is None: + if command == "": async with session.get("https://c.xkcd.com/comic/random") as r: if r.status != 200: await ctx.send(f"failure: random xkcd request returned `{r.status}`") diff --git a/cogs/mod.py b/canary/cogs/mod.py similarity index 83% rename from cogs/mod.py rename to canary/cogs/mod.py index e25638194..dfd0a0125 100644 --- a/cogs/mod.py +++ b/canary/cogs/mod.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -16,12 +16,13 @@ # along with Canary. If not, see . import discord -import sqlite3 import random from bidict import bidict from discord import utils from discord.ext import commands, tasks +from ..bot import Canary +from .base_cog import CanaryCog from .utils.checks import is_moderator from datetime import datetime, timedelta from .utils.role_restoration import ( @@ -35,32 +36,32 @@ from .utils.mock_context import MockContext -class Mod(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.guild: discord.Guild | None = None +class Mod(CanaryCog): + def __init__(self, bot: Canary) -> None: + super().__init__(bot) + self.verification_channel: discord.TextChannel | None = None self.last_verification_purge_datetime: datetime | None = None self.muted_users_to_appeal_channels: bidict = bidict() self.appeals_log_channel: discord.TextChannel | None = None self.muted_role: discord.Role | None = None - @commands.Cog.listener() + @CanaryCog.listener() async def on_ready(self): - self.guild = self.bot.get_guild(self.bot.config.server_id) + await super().on_ready() # TODO: needed? + + if not self.guild: + return await self.verification_purge_startup() - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT * FROM MutedUsers") self.muted_users_to_appeal_channels = bidict( [ (self.bot.get_user(user_id), self.bot.get_channel(appeal_channel_id)) - for (user_id, appeal_channel_id, roles, date) in c.fetchall() + for (user_id, appeal_channel_id, roles, date) in (await self.fetch_list("SELECT * FROM MutedUsers")) ] ) - conn.close() + self.appeals_log_channel = utils.get(self.guild.text_channels, name=self.bot.config.appeals_log_channel) self.muted_role = utils.get(self.guild.roles, name=self.bot.config.muted_role) @@ -68,25 +69,20 @@ async def verification_purge_startup(self): self.verification_channel = utils.get(self.guild.text_channels, name=self.bot.config.verification_channel) if not self.verification_channel: return - # arbitrary min date because choosing dates that predate discord will cause an httpexception + + # arbitrary min date because choosing dates that predate discord will cause an HTTPException # when fetching message history after that date later on self.last_verification_purge_datetime = datetime(2018, 1, 1) - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("last_verification_purge_timestamp",)) - fetched = c.fetchone() - if fetched: - last_purge_timestamp = float(fetched[0]) - if last_purge_timestamp: + + last_purge_timestamp = await self.get_settings_key("last_verification_purge_timestamp", deserialize=float) + if last_purge_timestamp is not None: + if last_purge_timestamp: # Not 0 self.last_verification_purge_datetime = datetime.fromtimestamp(last_purge_timestamp) else: # the verification purge info setting has not been added to db yet - c.execute( - "INSERT INTO Settings VALUES (?, ?)", - ("last_verification_purge_timestamp", self.last_verification_purge_datetime.timestamp()), + await self.set_settings_key( + "last_verification_purge_timestamp", self.last_verification_purge_datetime.timestamp() ) - conn.commit() - conn.close() self.check_verification_purge.start() @@ -99,22 +95,14 @@ async def check_verification_purge(self): if datetime.now() < self.last_verification_purge_datetime + timedelta(days=7): return - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() # delete everything since the day of the last purge, including that day itself await self.verification_purge_utility(self.last_verification_purge_datetime - timedelta(days=1)) # update info - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("last_verification_purge_timestamp",)) - fetched = c.fetchone() - if fetched: - c.execute( - "REPLACE INTO Settings VALUES (?, ?)", ("last_verification_purge_timestamp", datetime.now().timestamp()) - ) - conn.commit() - conn.close() + if (await self.get_settings_key("last_verification_purge_timestamp")) is not None: + await self.set_settings_key("last_verification_purge_timestamp", datetime.now().timestamp()) @commands.command() - async def answer(self, ctx, *args): + async def answer(self, ctx: commands.Context, *args): if isinstance(ctx.message.channel, discord.DMChannel): channel_to_send = utils.get( self.bot.get_guild(self.bot.config.server_id).text_channels, name=self.bot.config.reception_channel @@ -126,7 +114,7 @@ async def answer(self, ctx, *args): @commands.command(aliases=["dm"]) @is_moderator() - async def pm(self, ctx, user: discord.User, *, message): + async def pm(self, ctx: commands.Context, user: discord.User, *, message): """ PM a user on the server using the bot """ @@ -144,50 +132,46 @@ async def pm(self, ctx, user: discord.User, *, message): @commands.command() @is_moderator() - async def initiate_crabbo(self, ctx): + async def initiate_crabbo(self, ctx: commands.Context): """Initiates secret crabbo ceremony""" - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("CrabboMsgID",)) - if c.fetchone(): + if (await self.get_settings_key("CrabboMsgID")) is not None: await ctx.send("secret crabbo has already been started.") - conn.close() return + crabbo_msg = await ctx.send( "🦀🦀🦀 crabbo time 🦀🦀🦀\n<@&" f"{discord.utils.get(ctx.guild.roles, name=self.bot.config.crabbo_role).id}" "> react to this message with 🦀 to enter the secret crabbo festival\n" "🦀🦀🦀 crabbo time 🦀🦀🦀" ) - c.execute("REPLACE INTO Settings VALUES (?, ?)", ("CrabboMsgID", crabbo_msg.id)) - conn.commit() - conn.close() + + await self.set_settings_key("CrabboMsgID", crabbo_msg.id) await ctx.message.delete() @commands.command() @is_moderator() - async def finalize_crabbo(self, ctx): + async def finalize_crabbo(self, ctx: commands.Context): """Sends crabbos their secret crabbo""" - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT Value FROM Settings WHERE Key = ?", ("CrabboMsgID",)) - msg_id = c.fetchone() - c.execute("DELETE FROM Settings WHERE Key = ?", ("CrabboMsgID",)) - conn.commit() - conn.close() - if not msg_id: + msg_id = await self.get_settings_key("CrabboMsgId", deserialize=int) + + if msg_id is None: await ctx.send("secret crabbo is not currently occurring.") return - crabbos = None - for react in (await ctx.fetch_message(int(msg_id[0]))).reactions: + + await self.del_settings_key("CrabboMsgId") + + crabbos = [] + for react in (await ctx.fetch_message(msg_id)).reactions: if str(react) == "🦀": crabbos = await react.users().flatten() break - if crabbos is None or (num_crabbos := len(crabbos)) < 2: + + if (num_crabbos := len(crabbos)) < 2: await ctx.send("not enough people participated in the secret crabbo festival.") return + random.shuffle(crabbos) for index, crabbo in enumerate(crabbos): await self.bot.get_user(crabbo.id).send( @@ -197,13 +181,17 @@ async def finalize_crabbo(self, ctx): await ctx.message.delete() async def verification_purge_utility(self, after: datetime | discord.Message | None): - # after can be None, a datetime or a discord message + if not self.verification_channel: + return + await self.verification_channel.send("Starting verification purge") + channel = self.verification_channel deleted = 0 async for message in channel.history(oldest_first=True, limit=None, after=after): if message.attachments or message.embeds: content = message.content + if message.embeds: thumbnail_found = False for embed in message.embeds: @@ -212,11 +200,13 @@ async def verification_purge_utility(self, after: datetime | discord.Message | N content = content.replace(embed.thumbnail.url, "") if not thumbnail_found: continue + if content: await channel.send( f"{message.author} sent the following purged message on " f"{message.created_at.strftime('%Y/%m/%d, %H:%M:%S')}: {content}" ) + await message.delete() deleted += 1 @@ -226,31 +216,44 @@ async def verification_purge_utility(self, after: datetime | discord.Message | N @commands.command() @is_moderator() - async def verification_purge(self, ctx, id: int = None): - """ " + async def verification_purge(self, ctx: commands.Context, id_: int | None = None): + """ Manually start the purge of pictures in the verification channel. - If a message ID is provided, every pictures after that message will be removed. + If a message ID is provided, every picture after that message will be removed. If no message ID is provided, this will be done for the whole channel (may take time). """ + + if not self.guild: + return + if not self.bot.config.verification_channel: await ctx.send("No verification channel set in config file") return + if not self.verification_channel: # if no verification_channel was found on startup, we try to see if it exists now self.verification_channel = utils.get(self.guild.text_channels, name=self.bot.config.verification_channel) if not self.verification_channel: await ctx.send(f"Cannot find verification channel named {self.bot.config.verification_channel}") return + message = None - if id is not None: - message = await self.verification_channel.fetch_message(id) + if id_ is not None: + message = await self.verification_channel.fetch_message(id_) + await self.verification_purge_utility(message) - async def mute_utility(self, user: discord.Member, ctx=None): + async def mute_utility(self, user: discord.Member, ctx: commands.Context | MockContext | None = None): + if not self.guild: + return + + if not self.appeals_log_channel: + return + # note that this is made such that if a user is already muted # we make sure the user still has the role, is still in the db, and still has a channel - confirmation_channel = ctx.channel if ctx else self.appeals_log_channel + confirmation_channel: discord.TextChannel = ctx.channel if ctx else self.appeals_log_channel appeals_category = utils.get(self.guild.categories, name=self.bot.config.appeals_category) moderator_role = utils.get(self.guild.roles, name=self.bot.config.moderator_role) reason_message = ( @@ -295,7 +298,7 @@ async def mute_utility(self, user: discord.Member, ctx=None): # save existing roles and add muted user to database (with the attached appeal channel) # note that this function is such that if the user was already in the db, only the appeal channel is updated # (i.e, the situation where a mod had manually deleted the appeal channel) - save_existing_roles(self.bot, user, muted=True, appeal_channel=channel) + await save_existing_roles(self.bot, user, muted=True, appeal_channel=channel) # Remove all roles failed_roles: list[str] = [] @@ -306,6 +309,7 @@ async def mute_utility(self, user: discord.Member, ctx=None): await user.remove_roles(role, reason=reason_message) except (discord.Forbidden, discord.HTTPException): failed_roles.append(str(role)) + # update dict self.muted_users_to_appeal_channels[user] = channel @@ -327,6 +331,10 @@ async def mute_utility(self, user: discord.Member, ctx=None): async def unmute_utility(self, user: discord.Member, ctx: discord.ext.commands.Context | MockContext | None = None): confirmation_channel = ctx.channel if ctx else self.appeals_log_channel + + if not confirmation_channel: + return + reason_message = ( f"{ctx.author} used the unmute function on {user}" if ctx @@ -334,7 +342,7 @@ async def unmute_utility(self, user: discord.Member, ctx: discord.ext.commands.C ) # Restore old roles from the database - valid_roles = fetch_saved_roles(self.bot, self.guild, user, muted=True) + valid_roles = await fetch_saved_roles(self.bot, self.guild, user, muted=True) # for the following, if ctx is provided then the optional bot, guild, channel and restored_by values are ignored # if there is no ctx, it means that the user was unmuted because a mod removed the role manually # to know which mod did it, we would have to go through the audit log and try the find the log entry. Instead, @@ -348,7 +356,7 @@ async def unmute_utility(self, user: discord.Member, ctx: discord.ext.commands.C ) # Remove entry from the database - remove_from_muted_table(self.bot, user) + await remove_from_muted_table(self.bot, user) # Delete appeal channel if user in self.muted_users_to_appeal_channels: @@ -368,7 +376,7 @@ async def unmute_utility(self, user: discord.Member, ctx: discord.ext.commands.C @commands.command() @is_moderator() - async def mute(self, ctx, user: discord.Member): + async def mute(self, ctx: commands.Context, user: discord.Member): """ Mute a user and create an appeal channel (mod-only). The user's current roles are saved. @@ -378,7 +386,7 @@ async def mute(self, ctx, user: discord.Member): @commands.command() @is_moderator() - async def unmute(self, ctx, user: discord.Member): + async def unmute(self, ctx: commands.Context, user: discord.Member): """ Unmute a user and delete the appeal channel (mod-only). The user's previous roles are restored after confirmation. @@ -387,7 +395,7 @@ async def unmute(self, ctx, user: discord.Member): """ await self.unmute_utility(user, ctx=ctx) - @commands.Cog.listener() + @CanaryCog.listener() async def on_member_update(self, before, after): muted_role_before = self.muted_role in before.roles muted_role_after = self.muted_role in after.roles @@ -397,8 +405,8 @@ async def on_member_update(self, before, after): not muted_role_before and muted_role_after and not ( - is_in_muted_table(self.bot, after) - and has_muted_role(after) + (await is_in_muted_table(self.bot, after)) + and has_muted_role(self.bot, after) and after in self.muted_users_to_appeal_channels and self.muted_users_to_appeal_channels[after] in self.guild.text_channels ) @@ -410,15 +418,15 @@ async def on_member_update(self, before, after): muted_role_before and not muted_role_after and ( - is_in_muted_table(self.bot, after) - or has_muted_role(after) + (await is_in_muted_table(self.bot, after)) + or has_muted_role(self.bot, after) or after in self.muted_users_to_appeal_channels ) ): await self.unmute_utility(after) # the next three functions are used for appeals logging - @commands.Cog.listener() + @CanaryCog.listener() async def on_message(self, message): if message.channel not in self.muted_users_to_appeal_channels.values(): return @@ -441,8 +449,8 @@ async def on_message(self, message): await self.appeals_log_channel.send(log_message) - @commands.Cog.listener() - async def on_message_edit(self, before, after): + @CanaryCog.listener() + async def on_message_edit(self, _before, after): if after.channel not in self.muted_users_to_appeal_channels.values(): return @@ -464,7 +472,7 @@ async def on_message_edit(self, before, after): await self.appeals_log_channel.send(log_message) - @commands.Cog.listener() + @CanaryCog.listener() async def on_message_delete(self, message): if message.channel not in self.muted_users_to_appeal_channels.values(): return @@ -484,7 +492,7 @@ async def on_message_delete(self, message): await self.appeals_log_channel.send(log_message) - @commands.Cog.listener() + @CanaryCog.listener() async def on_member_join(self, user: discord.Member): # If the user was already muted, restore the muted role if is_in_muted_table(self.bot, user): diff --git a/cogs/music.py b/canary/cogs/music.py similarity index 90% rename from cogs/music.py rename to canary/cogs/music.py index 06ad1ee5a..29b632287 100644 --- a/cogs/music.py +++ b/canary/cogs/music.py @@ -18,16 +18,20 @@ # TODO: determine and (if possible) fix source of lack of precision in relative time skips import asyncio +import discord import random import time +import yt_dlp + +from collections import deque +from discord.ext import commands +from functools import partial from itertools import chain from inspect import getdoc -from functools import partial -from collections import deque from typing import Callable, Optional, Iterable -import discord -import yt_dlp -from discord.ext import commands + +from ..bot import Canary +from .base_cog import CanaryCog from .utils.music_helpers import ( FFMPEG_BEFORE_OPTS, FFMPEG_OPTS, @@ -46,22 +50,23 @@ ) -class Music(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.track_queue: deque = deque() - self.track_history: deque = deque() +class Music(CanaryCog): + def __init__(self, bot: Canary): + super().__init__(bot) + + self.track_queue: deque[tuple[dict, str]] = deque() + self.track_history: deque[tuple[dict, str]] = deque() self.looping_queue: bool = False self.track_lock: asyncio.Lock = asyncio.Lock() - self.playing = None - self.looping_track = False - self.volume_level: float = self.bot.config.music["start_vol"] + self.playing: tuple[dict, str] | None = None + self.looping_track: bool = False + self.volume_level: float = self.bot.config.music.start_vol self.speed_flag: str = "atempo=1" self.speed_val: float = 1.0 self.skip_opts: Optional[tuple[str, int]] = None self.track_start_time: float = 0.0 - self.ban_role = self.bot.config.music["ban_role"] - self.pause_start: Optional[float] = None + self.ban_role: str = self.bot.config.music.ban_role + self.pause_start: float | None = None async def get_info(self, url: str): try: @@ -97,6 +102,9 @@ def total_len(self) -> int: return len(self.track_queue) + len(self.track_history) if self.looping_queue else len(self.track_queue) def play_track(self, ctx, *, skip_str: Optional[str] = None, delta: int = 0): + if self.playing is None: + return + ctx.voice_client.play( discord.PCMVolumeTransformer( discord.FFmpegPCMAudio( @@ -110,7 +118,7 @@ def play_track(self, ctx, *, skip_str: Optional[str] = None, delta: int = 0): ) self.track_start_time = time.perf_counter() - delta - def from_total(self, idx): + def from_total(self, idx: int) -> tuple[deque[tuple[dict, str]], int] | None: q_len: int = self.total_len() if idx >= q_len or idx < -q_len: return None @@ -124,43 +132,43 @@ def from_total(self, idx): def subc_decision(self, subc: str) -> tuple[Callable, Optional[Callable]]: match subc: case "play": - return (self.play, lambda x: x) + return self.play, lambda x: x case "playback_speed" | "playbackspeed" | "ps" | "speed": - return (self.playback_speed, conv_arg(float, True)) + return self.playback_speed, conv_arg(float, True) case "goto_time" | "gototime" | "gt": - return (self.goto_time, conv_arg(lambda x: x, True)) + return self.goto_time, conv_arg(lambda x: x, True) case "forward_time" | "forwardtime" | "ft": - return (self.forward_time, conv_arg(lambda x: x, True)) + return self.forward_time, conv_arg(lambda x: x, True) case "backward_time" | "backwardtime" | "bt" | "rewind": - return (self.backward_time, conv_arg(lambda x: "30" if x is None else x, False)) + return self.backward_time, conv_arg(lambda x: "30" if x is None else x, False) case "loop": - return (self.loop, conv_arg(lambda x: x, True)) + return self.loop, conv_arg(lambda x: x, True) case "print_queue" | "printqueue" | "pq": - return (self.print_queue, conv_arg(lambda x: 0 if x is None else int(x), False)) + return self.print_queue, conv_arg(lambda x: 0 if x is None else int(x), False) case "status" | "now_playing" | "nowplaying" | "np": - return (self.music_status, None) + return self.music_status, None case "remove" | "pop": - return (self.remove_track, conv_arg(int, True)) + return self.remove_track, conv_arg(int, True) case "insert": - return (self.insert_track, conv_arg(insert_converter, True)) + return self.insert_track, conv_arg(insert_converter, True) case "clear_queue" | "clearqueue" | "cq": - return (self.clear_queue, None) + return self.clear_queue, None case "clear_hist" | "clearhist" | "ch": - return (self.clear_hist, None) + return self.clear_hist, None case "queue" | "q": - return (self.queue_track, lambda x: x) + return self.queue_track, lambda x: x case "volume" | "vol" | "v": - return (self.volume, conv_arg(lambda x: float(x.strip("%")), True)) + return self.volume, conv_arg(lambda x: float(x.strip("%")), True) case "stop": - return (self.stop, None) + return self.stop, None case "skip" | "next": - return (self.skip, conv_arg(lambda x: 0 if x is None else int(x), False)) + return self.skip, conv_arg(lambda x: 0 if x is None else int(x), False) case "back" | "previous": - return (self.backtrack, conv_arg(lambda x: None if x is None else int(x), False)) + return self.backtrack, conv_arg(lambda x: None if x is None else int(x), False) case "pause": - return (self.pause, None) + return self.pause, None case "resume": - return (self.resume, None) + return self.resume, None case _: raise ValueError @@ -222,9 +230,9 @@ async def music(self, ctx, subcommand: Optional[str] = None, *, args: Optional[s await fn(ctx, converted) @check_banned - async def play(self, ctx, url: Optional[str] = None): + async def play(self, ctx: commands.Context, url: Optional[str] = None): """ - streams from a youtube url or track name, or if none is given, from the queue + streams from a YouTube url or track name, or if none is given, from the queue arguments: (optional: link or title of track) """ @@ -280,7 +288,6 @@ async def play(self, ctx, url: Optional[str] = None): break if self.skip_opts is None: - if ctx.voice_client is None: break @@ -313,7 +320,8 @@ async def play(self, ctx, url: Optional[str] = None): skip_str, delta = self.skip_opts self.play_track(ctx, skip_str=skip_str, delta=delta) if self.pause_start is not None: - ctx.voice_client.pause() + if ctx.voice_client is not None: + await ctx.voice_client.pause() self.pause_start -= delta self.skip_opts = None @@ -329,7 +337,7 @@ async def play(self, ctx, url: Optional[str] = None): self.playing = None self.track_lock.release() - self.volume_level = self.bot.config.music["start_vol"] + self.volume_level: float = self.bot.config.music.start_vol self.speed_flag = "atempo=1" @check_playing @@ -604,13 +612,13 @@ async def volume(self, ctx, new_vol: float): arguments: (float) """ - self.volume_level = max(0, min(500, new_vol)) + self.volume_level = max(0.0, min(500.0, new_vol)) ctx.voice_client.source.volume = self.volume_level / 100 await ctx.send(f"changed volume to {self.volume_level}%.") @check_playing @check_banned - async def stop(self, ctx): + async def stop(self, ctx: commands.Context): """ stops and disconnects the bot from voice arguments: none @@ -622,7 +630,7 @@ async def stop(self, ctx): @check_playing @check_banned - async def skip(self, ctx, queue_amount: int): + async def skip(self, ctx: commands.Context, queue_amount: int): """ skips some amount of songs in the queue arguments: (optional: int [defaults to 0, going forward a single track]) @@ -644,7 +652,7 @@ async def skip(self, ctx, queue_amount: int): @check_playing @check_banned - async def backtrack(self, ctx, queue_amount: Optional[int]): + async def backtrack(self, ctx: commands.Context, queue_amount: Optional[int]): """ goes backwards in history by some amount of tracks arguments: (optional: int [defaults to 0 if track has been playing for more than 10 seconds, otherwise 1]) @@ -672,7 +680,7 @@ async def backtrack(self, ctx, queue_amount: Optional[int]): @check_playing @check_banned - async def pause(self, ctx): + async def pause(self, ctx: commands.Context): """ pauses currently playing track arguments: none @@ -682,7 +690,7 @@ async def pause(self, ctx): self.pause_start = time.perf_counter() await ctx.send("paused current track.") - async def resume(self, ctx): + async def resume(self, ctx: commands.Context): """ resumes currently paused track arguments: none diff --git a/cogs/quotes.py b/canary/cogs/quotes.py similarity index 63% rename from cogs/quotes.py rename to canary/cogs/quotes.py index f161ab6ce..a459c5551 100644 --- a/cogs/quotes.py +++ b/canary/cogs/quotes.py @@ -15,20 +15,18 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -# discord.py requirements -import discord -from discord.ext import commands +import aiosqlite import asyncio - -# For DB functionality -import sqlite3 - -# For Markov Chain +import discord import numpy as np +import random import re -# Other utils -import random +from discord.ext import commands +from typing import Optional + +from ..bot import Canary +from .base_cog import CanaryCog from .utils.paginator import Pages GEN_SPACE_SYMBOLS = re.compile(r"[,“”\".?!]") @@ -39,25 +37,27 @@ DEFAULT_AVATAR = "https://cdn.discordapp.com/embed/avatars/0.png" -class Quotes(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.mc_table = {} - self.rebuild_mc() +class Quotes(CanaryCog): + def __init__(self, bot: Canary): + super().__init__(bot) + self.mc_table: dict[str, dict] = {} - def rebuild_mc(self): + @CanaryCog.listener() + async def on_ready(self): + await super().on_ready() + await self.rebuild_mc() + + async def rebuild_mc(self): """ Rebuilds the Markov Chain lookup table for use with the ?generate command. Blame David for this code. """ - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT Quote FROM Quotes") - lookup = {} + all_quotes: list[tuple[str]] = await self.fetch_list("SELECT Quote FROM Quotes") + lookup: dict[str, dict] = {} - for q in c.fetchall(): + for q in all_quotes: # Skip URL quotes if re.search(r"https?://", q[0]): continue @@ -96,98 +96,101 @@ def rebuild_mc(self): lookup[word][option] = lookup[word][option] / total self.mc_table = lookup - conn.close() @commands.command(aliases=["addq"]) - async def add_quotes(self, ctx, member: discord.Member = None, *, quote: str = None): + async def add_quote(self, ctx: commands.Context, member: Optional[discord.Member] = None, *, quote: Optional[str]): """ Add a quote to a user's quote database. """ + replying: bool = ctx.message.reference and ctx.message.reference.resolved + if quote is None: if not replying: return member = member or ctx.message.reference.resolved.author quote = ctx.message.reference.resolved.content - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - t = (member.id, member.name, quote, str(ctx.message.created_at)) - c.execute("INSERT INTO Quotes VALUES (?,?,?,?)", t) - msg = await ctx.send("Quote added.") - - conn.commit() - - # Rebuild the Markov Chain lookup table to include new quote data. - self.rebuild_mc() - - await msg.add_reaction("🚮") - - def check(reaction, user): - # returns True if all the following is true: - # The user who reacted is either the quoter or the quoted person - # The user who reacted isn't the bot - # The react is the delete emoji - # The react is on the "Quote added." message - return ( - (user == ctx.message.author or user == member) - and user != self.bot.user - and str(reaction.emoji) == "🚮" - and reaction.message.id == msg.id + + if member is None: + return + + db: aiosqlite.Connection + async with self.db() as db: + await db.execute( + "INSERT INTO Quotes VALUES (?,?,?,?)", (member.id, member.name, quote, str(ctx.message.created_at)) ) - try: - await self.bot.wait_for("reaction_add", check=check, timeout=120) + msg = await ctx.send("Quote added.") - except asyncio.TimeoutError: - await msg.remove_reaction("🚮", self.bot.user) + await db.commit() - else: - t = (member.id, quote) - c.execute("DELETE FROM Quotes WHERE ID = ? AND Quote = ?", t) - conn.commit() - self.rebuild_mc() - await msg.delete() - await ctx.send("`Quote deleted.`", delete_after=60) + # Rebuild the Markov Chain lookup table to include new quote data. + await self.rebuild_mc() + + await msg.add_reaction("🚮") - conn.close() + def check(reaction, user): + # returns True if all the following is true: + # The user who reacted is either the quoter or the quoted person + # The user who reacted isn't the bot + # The reaction is the Delete emoji + # The reaction is on the "Quote added." message + return ( + (user == ctx.message.author or user == member) + and user != self.bot.user + and str(reaction.emoji) == "🚮" + and reaction.message.id == msg.id + ) + + try: + await self.bot.wait_for("reaction_add", check=check, timeout=120) + + except asyncio.TimeoutError: + await msg.remove_reaction("🚮", self.bot.user) + + else: + await db.execute("DELETE FROM Quotes WHERE ID = ? AND Quote = ?", (member.id, quote)) + await db.commit() + await self.rebuild_mc() + await msg.delete() + await ctx.send("`Quote deleted.`", delete_after=60) @commands.command(aliases=["q"]) - async def quotes(self, ctx, str1: str = None, *, str2: str = None): + async def quotes(self, ctx, str1: str | None = None, *, str2: str | None = None): """ Retrieve a quote with a specified keyword / mention. Can optionally use regex by surrounding the the query with /.../. """ - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + quotes: list[tuple[int, str, str]] + mentions = ctx.message.mentions if str1 is None: # No argument passed - quotes = c.execute("SELECT ID, Name, Quote FROM Quotes").fetchall() + quotes = await self.fetch_list("SELECT ID, Name, Quote FROM Quotes") elif mentions and mentions[0].mention == str1: # Has args u_id = mentions[0].id # Query for either user and quote or user only (None) - c.execute( - "SELECT ID, Name, Quote FROM Quotes WHERE ID = ? AND Quote " "LIKE ?", + quotes = await self.fetch_list( + "SELECT ID, Name, Quote FROM Quotes WHERE ID = ? AND Quote LIKE ?", (u_id, f"%{str2 if str2 is not None else ''}%"), ) - quotes = c.fetchall() else: # query for quote only query = str1 if str2 is None else f"{str1} {str2}" if query[0] == "/" and query[-1] == "/": - c.execute("SELECT ID, Name, Quote FROM Quotes") - quotes = c.fetchall() try: - quotes = [q for q in quotes if re.search(query[1:-1], q[2])] + quotes = [ + q + for q in (await self.fetch_list("SELECT ID, Name, Quote FROM Quotes")) + if re.search(query[1:-1], q[2]) + ] except re.error: - conn.close() await ctx.send("Invalid regex syntax.") return else: - c.execute("SELECT ID, Name, Quote FROM Quotes WHERE Quote LIKE ?", (f"%{query}%",)) - quotes = c.fetchall() + quotes = await self.fetch_list("SELECT ID, Name, Quote FROM Quotes WHERE Quote LIKE ?", (f"%{query}%",)) if not quotes: msg = await ctx.send("Quote not found.\n") @@ -196,8 +199,8 @@ async def quotes(self, ctx, str1: str = None, *, str2: str = None): def check(reaction, user): # returns True if all the following is true: # The user who reacted isn't the bot - # The react is the ok emoji - # The react is on the "Quote not found." message + # The reaction is the ok emoji + # The reaction is on the "Quote not found." message return (user == ctx.message.author and user != self.bot.user) and ( str(reaction.emoji) == "🆗" and reaction.message.id == msg.id ) @@ -212,11 +215,9 @@ def check(reaction, user): await ctx.message.delete() await msg.delete() - conn.close() return - conn.close() - quote_tuple = random.choice(quotes) + quote_tuple: tuple[int, str, str] = random.choice(quotes) author_id = int(quote_tuple[0]) name = quote_tuple[1] quote = quote_tuple[2] @@ -239,21 +240,20 @@ def check(reaction, user): await ctx.send(embed=embed) @commands.command(aliases=["lq"]) - async def list_quotes(self, ctx, author: discord.Member = None): + async def list_quotes(self, ctx: commands.Context, author: Optional[discord.Member] = None): """ List quotes """ - await ctx.trigger_typing() + db: aiosqlite.Connection + c: aiosqlite.Cursor - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + await ctx.trigger_typing() quote_author = author if author else ctx.message.author author_id = quote_author.id - c.execute("SELECT * FROM Quotes WHERE ID = ?", (author_id,)) - quote_list = c.fetchall() + quote_list: list[tuple] = await self.fetch_list("SELECT * FROM Quotes WHERE ID = ?", (author_id,)) if not quote_list: await ctx.send("No quote found.", delete_after=60) return @@ -293,10 +293,13 @@ def msg_check(msg): if index == -1: await ctx.send("Exit delq.", delete_after=60) else: - t = (quote_list[index][0], quote_list[index][2]) + q = quote_list[index] + del quote_list[index] - c.execute("DELETE FROM Quotes WHERE ID = ? AND Quote = ?", t) - conn.commit() + + async with self.db() as db: + await db.execute("DELETE FROM Quotes WHERE ID = ? AND Quote = ?", (q[0], q[2])) + await db.commit() await ctx.send("Quote deleted", delete_after=60) await message.delete() @@ -305,11 +308,8 @@ def msg_check(msg): await p.paginate() - conn.commit() - conn.close() - @commands.command(aliases=["allq", "aq"]) - async def all_quotes(self, ctx, *, query): + async def all_quotes(self, ctx: commands.Context, *, query: str): """ List all quotes that contain the query string. Can optionally use regex by surrounding the the query with /.../. @@ -320,7 +320,7 @@ async def all_quotes(self, ctx, *, query): """ if not query: - ctx.send("You must provide a query") + await ctx.send("You must provide a query") return query_splitted = query.split() @@ -339,21 +339,16 @@ async def all_quotes(self, ctx, *, query): query = " ".join(query_splitted) await ctx.trigger_typing() - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - if query[0] == "/" and query[-1] == "/": - c.execute("SELECT * FROM Quotes") - quotes = c.fetchall() try: - quote_list = [q for q in quotes if re.search(query[1:-1], q[2])] + quote_list = [ + q for q in (await self.fetch_list("SELECT * FROM Quotes")) if re.search(query[1:-1], q[2]) + ] except re.error: - conn.close() await ctx.send("Invalid regex syntax.") return else: - c.execute("SELECT * FROM Quotes WHERE Quote LIKE ?", (f"%{query}%",)) - quote_list = c.fetchall() + quote_list = await self.fetch_list("SELECT * FROM Quotes WHERE Quote LIKE ?", (f"%{query}%",)) if not quote_list: await ctx.send("No quote found.", delete_after=60) @@ -372,7 +367,7 @@ async def all_quotes(self, ctx, *, query): await p.paginate() @commands.command(aliases=["gen"]) - async def generate(self, ctx, seed: str = None, min_length: int = 1): + async def generate(self, ctx: commands.Context, seed: str | None = None, min_length: int = 1): """ Generates a random 'quote' using a Markov Chain. Optionally takes in a word to seed the Markov Chain with and (also optionally) a desired @@ -393,52 +388,49 @@ async def generate(self, ctx, seed: str = None, min_length: int = 1): if seed is None: await ctx.send("Markov chain table is empty.", delete_after=60) - elif seed not in self.mc_table.keys(): + return + if seed not in self.mc_table.keys(): await ctx.send("Could not generate anything with that seed.", delete_after=60) - else: - longest_sentence = [] - retries = 0 - - while len(longest_sentence) < min_length and retries < 200: - current_word = seed - sentence = [current_word] - - # Add words to the sentence until a termination condition is - # encountered. - while True: - choices = [(w, self.mc_table[current_word][w]) for w in self.mc_table[current_word]] - c_words, p_dist = zip(*choices) - - # Choose a random word and add it to the sentence using the - # probability distribution stored in the word entry. - old_word = current_word - current_word = np.random.choice(c_words, p=p_dist) + return + + longest_sentence: list[str] = [] + retries = 0 - # Don't allow termination until the minimum length is met - # or we don't have any other option. - while ( - current_word == "TERM" - and len(sentence) < min_length - and len(self.mc_table[old_word].keys()) > 1 - ): - current_word = np.random.choice(c_words, p=p_dist) + while len(longest_sentence) < min_length and retries < 200: + current_word: str = seed + sentence: list[str] = [current_word] - # Don't allow repeat words too much - while len(sentence) >= 3 and (current_word == sentence[-1] == sentence[-2] == sentence[-3]): - current_word = np.random.choice(c_words, p=p_dist) + # Add words to the sentence until a termination condition is + # encountered. + while True: + choices = [(w, self.mc_table[current_word][w]) for w in self.mc_table[current_word]] + c_words, p_dist = zip(*choices) + + # Choose a random word and add it to the sentence using the + # probability distribution stored in the word entry. + old_word: str = current_word + current_word = np.random.choice(c_words, p=p_dist) + + # Don't allow termination until the minimum length is met, or we don't have any other option. + while current_word == "TERM" and len(sentence) < min_length and len(self.mc_table[old_word].keys()) > 1: + current_word = np.random.choice(c_words, p=p_dist) + + # Don't allow repeat words too much + while len(sentence) >= 3 and (current_word == sentence[-1] == sentence[-2] == sentence[-3]): + current_word = np.random.choice(c_words, p=p_dist) - # Cap sentence at 1000 words, just in case, and terminate - # if termination symbol is seen. - if current_word == "TERM" or len(sentence) >= 1000: - break - sentence.append(current_word) + # Cap sentence at 1000 words, just in case, and terminate + # if termination symbol is seen. + if current_word == "TERM" or len(sentence) >= 1000: + break + sentence.append(current_word) - if len(longest_sentence) < len(sentence) and len(" ".join(sentence)) <= 2000: - longest_sentence = sentence[:] + if len(longest_sentence) < len(sentence) and len(" ".join(sentence)) <= 2000: + longest_sentence = sentence[:] - retries += 1 + retries += 1 - await ctx.send(" ".join(longest_sentence)) + await ctx.send(" ".join(longest_sentence)) def setup(bot): diff --git a/cogs/reminder.py b/canary/cogs/reminder.py similarity index 56% rename from cogs/reminder.py rename to canary/cogs/reminder.py index dcf111c1a..56a68e42b 100644 --- a/cogs/reminder.py +++ b/canary/cogs/reminder.py @@ -15,20 +15,17 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . -# discord-py requirements -import discord -from discord.ext import commands +import aiosqlite import asyncio +import discord +import re -# For DB Functionality -import sqlite3 -import datetime +from datetime import datetime, timedelta +from discord.ext import commands -# Other utilities +from .base_cog import CanaryCog from .utils.paginator import Pages -# For remindme functionality -import re ONES_NAMES = ("one", "two", "three", "four", "five", "six", "seven", "eight", "nine") # Haha English start at 20 @@ -46,7 +43,7 @@ ("no", "0"), ("none", "0"), ("zero", "0"), - *(zip(ONES_NAMES, range(1, 10))), + *zip(ONES_NAMES, range(1, 10)), ("ten", "10"), ("eleven", "11"), ("twelve", "12"), @@ -88,12 +85,10 @@ # Regex for time HH:MM HM_REGEX = re.compile(r"\b([0-1]?[0-9]|2[0-4]):([0-5][0-9])") +FREQUENCIES = {"daily": 1, "weekly": 7, "monthly": 30} -class Reminder(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.frequencies = {"daily": 1, "weekly": 7, "monthly": 30} +class Reminder(CanaryCog): async def check_reminders(self): """ Co-routine that periodically checks if the bot must issue reminders to @@ -103,60 +98,60 @@ async def check_reminders(self): await self.bot.wait_until_ready() while not self.bot.is_closed(): - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - guild = self.bot.get_guild(self.bot.config.server_id) - if not guild: + if not (guild := self.guild): return - reminders = c.execute("SELECT * FROM Reminders").fetchall() - for i in range(len(reminders)): - member = discord.utils.get(guild.members, id=reminders[i][0]) - - # If non-repeating reminder is found - if reminders[i][3] == "once": - # Check date to remind user - reminder_activation_date = datetime.datetime.strptime(reminders[i][4], "%Y-%m-%d %H:%M:%S.%f") - # Compute future_date and current_date and if past, means - # time is due to remind user. - if reminder_activation_date <= datetime.datetime.now(): - await member.send("Reminding you to {}!".format(reminders[i][2])) - # Remove from from DB non-repeating reminder - c.execute( - ("DELETE FROM Reminders WHERE Reminder=? AND ID=?" + " AND DATE=?"), - (reminders[i][2], reminders[i][0], reminder_activation_date), - ) - conn.commit() - await asyncio.sleep(1) - - else: - last_date = datetime.datetime.strptime(reminders[i][5], "%Y-%m-%d %H:%M:%S.%f") - - if datetime.datetime.now() - last_date > datetime.timedelta(days=self.frequencies[reminders[i][3]]): - await member.send(f"Reminding you to {reminders[i][2]}! [{i + 1:d}]") - c.execute( - ("UPDATE 'Reminders' SET LastReminder=? WHERE " + "Reminder=?"), - (datetime.datetime.now(), reminders[i][2]), - ) - conn.commit() - await asyncio.sleep(1) + async with self.db() as db: + reminders = await self.fetch_list("SELECT * FROM Reminders", db=db) + + for i, reminder in enumerate(reminders): + member = discord.utils.get(guild.members, id=reminder[0]) + now = datetime.now() + + # If non-repeating reminder is found + if reminder[3] == "once": + # Check date to remind user + reminder_activation_date = datetime.strptime(reminder[4], "%Y-%m-%d %H:%M:%S.%f") + # Compute future_date and current_date and if past, means + # time is due to remind user. + if reminder_activation_date <= now: + await member.send(f"Reminding you to {reminder[2]}!") + # Remove from from DB non-repeating reminder + await db.execute( + "DELETE FROM Reminders WHERE Reminder=? AND ID=? AND DATE=?", + (reminder[2], reminder[0], reminder_activation_date), + ) + await db.commit() + await asyncio.sleep(1) + + else: + last_date = datetime.strptime(reminder[5], "%Y-%m-%d %H:%M:%S.%f") + + if now - last_date > timedelta(days=FREQUENCIES[reminders[i][3]]): + await member.send(f"Reminding you to {reminder[2]}! [{i + 1:d}]") + await db.execute("UPDATE Reminders SET LastReminder=? WHERE Reminder=?", (now, reminder[2])) + await db.commit() + await asyncio.sleep(1) - conn.close() await asyncio.sleep(60) # seconds @commands.command(aliases=["rm", "rem"]) - async def remindme(self, ctx, *, quote: str = ""): + async def remindme(self, ctx: commands.Context, *, quote: str = ""): """ Parses the reminder and adds a one-time reminder to the reminder database or calls remindme_repeating to deal with repetitive reminders when keyword "daily", "weekly" or "monthly" is found. """ + db: aiosqlite.Connection + c: aiosqlite.Cursor + if quote == "": await ctx.send( - "**Usage:** \n`?remindme in 1 hour and 20 minutes and 20 " - "seconds to eat` **or** \n `?remindme at 2020-04-30 11:30 to " - "graduate` **or** \n`?remindme daily to sleep`" + "**Usage:** \n" + "`?remindme in 1 hour and 20 minutes and 20 seconds to eat` **or** \n" + "`?remindme at 2020-04-30 11:30 to graduate` **or** \n" + "`?remindme daily to sleep`" ) return @@ -199,9 +194,9 @@ async def remindme(self, ctx, *, quote: str = ""): for segment in input_segments: if re.match(TIME_SEPARATOR_REGEX, segment): continue - if re.match(f"^{NUMBER_REGEX}$", segment): + if re.match(rf"^{NUMBER_REGEX}$", segment): last_number = segment - elif re.match(f"^{UNIT_REGEX}$", segment): + elif re.match(rf"^{UNIT_REGEX}$", segment): time_segments.append(f"{last_number} {segment}") else: first_reminder_segment = segment @@ -211,6 +206,8 @@ async def remindme(self, ctx, *, quote: str = ""): # and formatting, so extract from original string. reminder = quote[quote.index(first_reminder_segment) :] + msg_author = ctx.message.author + # Date-based reminder triggered by "at" and "on" keywords if input_segments[0] in {"at", "on"}: # Gets YYYY-mm-dd @@ -218,85 +215,81 @@ async def remindme(self, ctx, *, quote: str = ""): # Gets HH:MM time_result = re.search(HM_REGEX, original_input_copy) - # If both a date and a time is found, continue - if date_result and time_result: - # Compute datetime.Object - absolute_duedate = datetime.datetime.strptime( - "{Y}-{m}-{d}-{H}-{M}-{S}".format( - Y=date_result.group(1), - m=date_result.group(2), - d=date_result.group(4), - H=time_result.group(1), - M=time_result.group(2), - S=0.1, - ), - "%Y-%m-%d-%H-%M-%S.%f", - ) + if date_result is None or time_result is None: + # Wrong input feedback depending on what is missing. + await ctx.send("Check your private messages for info on correct syntax!") + await ctx.author.send("Please double check the following: ") + if not date_result: + await ctx.author.send("Make sure you have specified a date in the format: `YYYY-mm-dd`") + if not time_result: + await ctx.author.send("Make sure you have specified a time in the 24H format: `HH:MM`") + await ctx.author.send("E.g.: `?remindme on 2020-12-05 at 21:44 to feed Marty`") + return - # Strips "to" and dates from the reminder message - time_input_end = time_result.span()[1] - if re.match("to", reminder[time_input_end : time_input_end + 4].strip(), re.IGNORECASE): - reminder = reminder[time_input_end + 3 :].strip() - else: - reminder = reminder[time_input_end + 1 :].strip() + # Otherwise, both a date and a time are found, so continue + + # Compute datetime.Object + absolute_duedate = datetime.strptime( + "{Y}-{m}-{d}-{H}-{M}-{S}".format( + Y=date_result.group(1), + m=date_result.group(2), + d=date_result.group(4), + H=time_result.group(1), + M=time_result.group(2), + S=0.1, + ), + "%Y-%m-%d-%H-%M-%S.%f", + ) + + # Strips "to" and dates from the reminder message + time_input_end = time_result.span()[1] + if re.match("to", reminder[time_input_end : time_input_end + 4].strip(), re.IGNORECASE): + reminder = reminder[time_input_end + 3 :].strip() + else: + reminder = reminder[time_input_end + 1 :].strip() - # Add message to database - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + # Add message to database + async with self.db() as db: t = ( - ctx.message.author.id, - ctx.message.author.name, + msg_author.id, + msg_author.name, reminder, "once", absolute_duedate, - datetime.datetime.now(), - ) - - c.execute("INSERT INTO Reminders VALUES (?, ?, ?, ?, ?, ?)", t) - - # Send user information and close database - reminders = c.execute("SELECT * FROM Reminders WHERE ID =?", (ctx.message.author.id,)).fetchall() - await ctx.author.send( - "Hi {}! \nI will remind you to {} on {} at {} unless you " - "send me a message to stop reminding you about it! " - "[{:d}]".format( - ctx.author.name, reminder, date_result.group(0), time_result.group(0), len(reminders) + 1 - ) + datetime.now(), ) + await db.execute("INSERT INTO Reminders VALUES (?, ?, ?, ?, ?, ?)", t) + await db.commit() - await ctx.send("Reminder added.") + # Send user information - conn.commit() - conn.close() + num_reminders = await self.get_num_reminders(db, msg_author) - return + await ctx.author.send( + f"Hi {ctx.author.name}! \nI will remind you to {reminder} on {date_result.group(0)} at " + f"{time_result.group(0)} unless you send me a message to stop reminding you about it! " + f"[{num_reminders + 1:d}]" + ) - # Wrong input feedback depending on what is missing. - await ctx.send("Check your private messages for info on correct syntax!") - await ctx.author.send("Please double check the following: ") - if not date_result: - await ctx.author.send(("Make sure you have specified a date in the format: " + "`YYYY-mm-dd`")) - if not time_result: - await ctx.author.send(("Make sure you have specified a time in the 24H format: " + "`HH:MM`")) - await ctx.author.send("E.g.: `?remindme on 2020-12-05 at 21:44 to feed Marty`") + await ctx.send("Reminder added.") return # Regex for the number and time units and store in "match" for segment in time_segments: - match = re.match(r"^({})\s+{}$".format(NUMBER_REGEX, UNIT_REGEX), segment) + match = re.match(rf"^({NUMBER_REGEX})\s+{UNIT_REGEX}$", segment) number = float(match.group(1)) # Regex potentially misspelled time units and match to proper # spelling. for regex in REMINDER_UNITS: - if re.match("^{}$".format(REMINDER_UNITS[regex]), match.group(3)): + if re.match(f"^{REMINDER_UNITS[regex]}$", match.group(3)): time_offset[regex] += number # Convert years to a unit that datetime will understand time_offset["days"] = time_offset["days"] + time_offset["years"] * 365 - time_now = datetime.datetime.now() # Current time - reminder_time = time_now + datetime.timedelta( + time_now = datetime.now() # Current time + reminder_time = time_now + timedelta( days=time_offset["days"], hours=time_offset["hours"], seconds=time_offset["seconds"], @@ -305,7 +298,7 @@ async def remindme(self, ctx, *, quote: str = ""): ) # Time to be reminded on if time_now == reminder_time: # No time in argument, or it's zero. - await ctx.send("Please specify a time! E.g.: `?remindme in 1 hour {}`".format(reminder)) + await ctx.send(f"Please specify a time! E.g.: `?remindme in 1 hour {reminder}`") return # Strips the string "to " from reminder messages @@ -314,30 +307,19 @@ async def remindme(self, ctx, *, quote: str = ""): # DB: Date will hold TDELTA (When reminder is due), LastReminder will # hold datetime.datetime.now() - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - t = (ctx.message.author.id, ctx.message.author.name, reminder, "once", reminder_time, time_now) - - reminders = c.execute("SELECT * FROM Reminders WHERE ID =?", (ctx.message.author.id,)).fetchall() - - c.execute("INSERT INTO Reminders VALUES (?, ?, ?, ?, ?, ?)", t) - - # Gets reminder date in YYYY-MM-DD format - due_date = str(datetime.date(reminder_time.year, reminder_time.month, reminder_time.day)) - - # Gets reminder time in HH:MM - due_time = str(reminder_time).split()[1].split(":")[0] + ":" + str(reminder_time).split()[1].split(":")[1] + async with self.db() as db: + num_reminders = await self.get_num_reminders(db, msg_author) + t = (msg_author.id, msg_author.name, reminder, "once", reminder_time, time_now) + await db.execute("INSERT INTO Reminders VALUES (?, ?, ?, ?, ?, ?)", t) + await db.commit() await ctx.author.send( f"Hi {ctx.author.name}! \nI will remind you to {reminder} on " - f"{due_date} at {due_time} unless you send me a message to stop " - f"reminding you about it! [{len(reminders)+1}]" + f"{reminder_time.strftime('%Y-%m-%d at %H:%M')} unless you send me a message to stop " + f"reminding you about it! [{num_reminders+1:d}]" ) await ctx.send("Reminder added.") - conn.commit() - conn.close() - @staticmethod def formatted_reminder_list(rem_list): return [ @@ -356,6 +338,9 @@ async def list_reminders(self, ctx): List reminders """ + db: aiosqlite.Connection + c: aiosqlite.Cursor + await ctx.trigger_typing() if not isinstance(ctx.message.channel, discord.DMChannel): @@ -365,24 +350,17 @@ async def list_reminders(self, ctx): ) return - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - rem_author = ctx.message.author - author_id = rem_author.id - c.execute("SELECT * FROM Reminders WHERE ID = ?", (author_id,)) - rem_list = c.fetchall() + rem_list = await self.fetch_list("SELECT * FROM Reminders WHERE ID = ?", (rem_author.id,)) if not rem_list: await ctx.send("No reminder found.", delete_after=60) - conn.commit() - conn.close() return p = Pages( ctx, item_list=self.formatted_reminder_list(rem_list), - title="{}'s reminders".format(rem_author.display_name), + title=f"{rem_author.display_name}'s reminders", ) await p.paginate() @@ -391,7 +369,7 @@ def msg_check(msg): try: return ( 0 <= int(msg.content) <= len(rem_list) - and msg.author.id == author_id + and msg.author.id == rem_author.id and msg.channel == ctx.message.channel ) @@ -420,12 +398,14 @@ def msg_check(msg): await ctx.send("Exit delq.", delete_after=60) else: - t = (rem_list[index][0], rem_list[index][2]) + r = rem_list[index] + # Remove deleted reminder from list: del rem_list[index] - c.execute("DELETE FROM Reminders WHERE ID = ? AND " "Reminder = ?", t) - conn.commit() + async with self.db() as db: + await db.execute("DELETE FROM Reminders WHERE ID = ? AND Reminder = ?", (r[0], r[2])) + await db.commit() await ctx.send("Reminder deleted", delete_after=60) @@ -433,24 +413,23 @@ def msg_check(msg): await p.paginate() - conn.commit() - conn.close() - - async def __remindme_repeating(self, ctx, freq: str = "", *, quote: str = ""): + async def __remindme_repeating(self, ctx: commands.Context, freq: str = "", *, quote: str = ""): """ Called by remindme to add a repeating reminder to the reminder database. """ + db: aiosqlite.Connection + bad_input = False freq = freq.strip() quote = quote.strip() - if freq not in self.frequencies.keys(): + if freq not in FREQUENCIES.keys(): await ctx.send( - "Please ensure you specify a frequency from the following " - "list: `daily`, `weekly`, `monthly`, before your message!" + "Please ensure you specify a frequency from the following list: " + "`daily`, `weekly`, `monthly`, before your message!" ) bad_input = True @@ -462,48 +441,40 @@ async def __remindme_repeating(self, ctx, freq: str = "", *, quote: str = ""): if bad_input: return - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + msg_author = ctx.message.author - t = ( - ctx.message.author.id, - ctx.message.author.name, - quote, - freq, - datetime.datetime.now(), - datetime.datetime.now(), + existing_reminder = await self.fetch_one( + "SELECT * FROM Reminders WHERE Reminder = ? AND ID = ?", (quote, msg_author.id) ) - - reminders = c.execute( - "SELECT * FROM Reminders WHERE Reminder = ? AND ID = ?", (quote, ctx.message.author.id) - ).fetchall() - - if len(reminders) > 0: + if existing_reminder is not None: await ctx.send( - "The reminder `{}` already exists in your database. Please " - "specify a unique reminder message!".format(quote) + f"The reminder `{quote}` already exists in your database. Please specify a unique reminder message!" ) - conn.commit() - conn.close() return - reminders = c.execute("SELECT * FROM Reminders WHERE ID = ?", (ctx.message.author.id,)).fetchall() - - c.execute("INSERT INTO Reminders VALUES (?, ?, ?, ?, ?, ?)", t) + now = datetime.now() + async with self.db() as db: + num_reminders = await self.get_num_reminders(db, msg_author) + await db.execute( + "INSERT INTO Reminders VALUES (?, ?, ?, ?, ?, ?)", + (msg_author.id, msg_author.name, quote, freq, now, now), + ) + await db.commit() # Strips the string "to " from reminder messages if quote[:3].lower() == "to ": quote = quote[3:] await ctx.author.send( - "Hi {}! \nI will remind you to {} {} until you send me a message " - "to stop reminding you about it! [{:d}]".format(ctx.author.name, quote, freq, len(reminders) + 1) + f"Hi {ctx.author.name}! \nI will remind you to {quote} {freq} until you send me a message " + f"to stop reminding you about it! [{num_reminders+1:d}]" ) await ctx.send("Reminder added.") - conn.commit() - conn.close() + async def get_num_reminders(self, db: aiosqlite.Connection, author: discord.Member | discord.User) -> int: + num_reminders_t = await self.fetch_one("SELECT COUNT(*) FROM Reminders WHERE ID = ?", (author.id,), db=db) + return num_reminders_t[0] if num_reminders_t is not None else 0 def setup(bot): diff --git a/cogs/roles.py b/canary/cogs/roles.py similarity index 83% rename from cogs/roles.py rename to canary/cogs/roles.py index 4115a5bc7..2989225dc 100644 --- a/cogs/roles.py +++ b/canary/cogs/roles.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -16,13 +16,14 @@ # along with Canary. If not, see . import discord -import sqlite3 from discord import utils from discord.ext import commands from enum import Enum from typing import Optional, Tuple +from ..bot import Canary +from .base_cog import CanaryCog from .utils.checks import is_moderator from .utils.paginator import Pages from .utils.role_restoration import save_existing_roles, fetch_saved_roles, is_in_muted_table, role_restoring_page @@ -33,7 +34,7 @@ class RoleTransaction(Enum): REMOVE = "remove" -class Roles(commands.Cog): +class Roles(CanaryCog): ALL_CATEGORIES = ( "pronouns", "fields", @@ -50,18 +51,23 @@ class Roles(commands.Cog): "generics": None, } - def __init__(self, bot): - self.bot = bot + def __init__(self, bot: Canary): + super().__init__(bot) + self.roles = self.bot.config.roles self.mod_role = self.bot.config.moderator_role @staticmethod - async def paginate_roles(ctx, roles, title="All roles in server"): + async def paginate_roles(ctx: commands.Context, roles, title="All roles in server"): p = Pages(ctx, item_list=[r + "\n" for r in roles], title=title, display_option=(3, 20), editable_content=False) await p.paginate() async def toggle_role( - self, ctx, transaction: RoleTransaction, requested_role: Optional[str], categories: Tuple[str, ...] + self, + ctx: commands.Context, + transaction: RoleTransaction, + requested_role: str, + categories: Tuple[str, ...], ): """ Assigns a single role to a user with no checks from a category of roles @@ -71,7 +77,7 @@ async def toggle_role( if not requested_role: roles = [] for c in categories: - roles += self.roles[c] # All roles in the category + roles += getattr(self.roles, c) # All roles in the category # If no role is specified, list what is available in all possible # categories for the command. @@ -81,12 +87,14 @@ async def toggle_role( # If a role is specified, narrow the category down to the one with the # role in it to impose a proper limit. try: - category = next((c for c in categories if requested_role.lower() in {r.lower() for r in self.roles[c]})) + category = next( + (c for c in categories if requested_role.lower() in {r.lower() for r in getattr(self.roles, c)}) + ) except StopIteration: await ctx.send(f"Invalid role for {fcategory} `{', '.join(categories)}`.") return - roles = self.roles[category] + roles = getattr(self.roles, category) roles_lower = [r.lower() for r in roles] requested_role = roles[roles_lower.index(requested_role.lower())] @@ -124,7 +132,7 @@ async def toggle_role( elif limit and len(existing_roles) == limit: await ctx.send( - f"You have too many roles in category " f"`{category}` (limit is `{limit}`). " f"Please remove one." + f"You have too many roles in category `{category}` (limit is `{limit}`). Please remove one." ) return @@ -148,7 +156,7 @@ async def add_role(self, ctx, requested_role: Optional[str], categories: Tuple[s return await self.toggle_role(ctx, RoleTransaction.ADD, requested_role, categories) @commands.command(aliases=["pronouns"]) - async def pronoun(self, ctx, *, pronoun: Optional[str] = None): + async def pronoun(self, ctx: commands.Context, *, pronoun: str = ""): """ Self-assign a pronoun role to a user. If no argument is given, returns a list of roles that can be used with this command. @@ -156,7 +164,7 @@ async def pronoun(self, ctx, *, pronoun: Optional[str] = None): await self.add_role(ctx, pronoun, ("pronouns",)) @commands.command(aliases=["fields", "program", "programs", "major", "majors"]) - async def field(self, ctx, *, field: Optional[str] = None): + async def field(self, ctx: commands.Context, *, field: str = ""): """ Self-assign a field of study role to a user. If no argument is given, returns a list of roles that can be used with this command. @@ -164,7 +172,7 @@ async def field(self, ctx, *, field: Optional[str] = None): await self.add_role(ctx, field, ("fields",)) @commands.command(aliases=["faculties"]) - async def faculty(self, ctx, *, faculty: Optional[str] = None): + async def faculty(self, ctx: commands.Context, *, faculty: str = ""): """ Self-assign a faculty of study role to a user. If no argument is given, returns a list of roles that can be used with this command. @@ -172,7 +180,7 @@ async def faculty(self, ctx, *, faculty: Optional[str] = None): await self.add_role(ctx, faculty, ("faculties",)) @commands.command(aliases=["years"]) - async def year(self, ctx, year: Optional[str] = None): + async def year(self, ctx, year: str = ""): """ Self-assign a year of study role to a user. If no argument is given, returns a list of roles that can be used with this command. @@ -180,7 +188,7 @@ async def year(self, ctx, year: Optional[str] = None): await Roles.add_role(self, ctx, year, ("years",)) @commands.command(aliases=["iam", "generic", "generics"]) - async def i_am(self, ctx, *, role: Optional[str]): + async def i_am(self, ctx: commands.Context, *, role: str = ""): """ Self-assign a generic role to a user. If no argument is given, returns a list of roles that can be used with this command. @@ -188,14 +196,14 @@ async def i_am(self, ctx, *, role: Optional[str]): await self.add_role(ctx, role, Roles.ALL_CATEGORIES) @commands.command(aliases=["iamn"]) - async def i_am_not(self, ctx, *, role: Optional[str]): + async def i_am_not(self, ctx: commands.Context, *, role: str = ""): """ Self-unassign a generic role to a user. """ await self.toggle_role(ctx, RoleTransaction.REMOVE, role, Roles.ALL_CATEGORIES) @commands.command() - async def roles(self, ctx, user: discord.Member = None): + async def roles(self, ctx, user: discord.Member = None): # bad type hinting, but needed for discord.py conversion """Returns list of all roles in server or the list of a specific user's roles""" role_names = [ @@ -209,7 +217,7 @@ async def roles(self, ctx, user: discord.Member = None): ) @commands.command(aliases=["inrole"]) - async def in_role(self, ctx, *, query_role): + async def in_role(self, ctx: commands.Context, *, query_role: str): """Returns list of users in the specified role""" role = next((role for role in ctx.guild.roles if role.name.lower() == query_role.lower()), None) @@ -232,8 +240,8 @@ async def in_role(self, ctx, *, query_role): @commands.command(aliases=["cr", "createrole"]) @is_moderator() - async def create_role(self, ctx, *, role: Optional[str] = None): - role = (role or "").strip() + async def create_role(self, ctx: commands.Context, *, role: str = ""): + role = role.strip() if not role: await ctx.send("Please specify a role name.") return @@ -247,7 +255,7 @@ async def create_role(self, ctx, *, role: Optional[str] = None): await ctx.send("Role created successfully.") @commands.command(aliases=["inchannel"]) - async def in_channel(self, ctx): + async def in_channel(self, ctx: commands.Context): """Returns list of users in current channel""" channel = ctx.message.channel members = channel.members @@ -259,7 +267,7 @@ async def in_channel(self, ctx): await pages.paginate() @commands.command(aliases=["previousroles", "giverolesback", "rolesback", "givebackroles"]) - async def previous_roles(self, ctx, user: discord.Member): + async def previous_roles(self, ctx: commands.Context, user: discord.Member): """Show the list of roles that a user had before leaving, if possible. A moderator can click the OK react on the message to give these roles back """ @@ -268,13 +276,13 @@ async def previous_roles(self, ctx, user: discord.Member): await ctx.send("Cannot restore roles to a muted user") return - valid_roles = fetch_saved_roles(self.bot, ctx.guild, user) + valid_roles = await fetch_saved_roles(self.bot, ctx.guild, user) await role_restoring_page(self.bot, ctx, user, valid_roles) @commands.Cog.listener() async def on_member_remove(self, user: discord.Member): # If the user is muted, this saves all roles BUT the muted role into the PreviousRoles table - save_existing_roles(self.bot, user, muted=is_in_muted_table(self.bot, user)) + await save_existing_roles(self.bot, user, muted=await is_in_muted_table(self.bot, user)) def setup(bot): diff --git a/cogs/score.py b/canary/cogs/score.py similarity index 81% rename from cogs/score.py rename to canary/cogs/score.py index b97b91413..116475cbf 100644 --- a/cogs/score.py +++ b/canary/cogs/score.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2021) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -23,9 +21,13 @@ import discord from discord.ext import commands +# For type hinting +from ..bot import Canary + # For DB functionality -import sqlite3 +import aiosqlite import json +from .base_cog import CanaryCog from .utils.members import add_member_if_needed # For argument parsing @@ -46,7 +48,7 @@ async def convert(self, ctx, argument): except commands.BadArgument: if not any(argument in d.values() for d in EMOJI.values()): raise commands.BadArgument( - "Not in the current list of Discord Unicode Emojis " "and no Custom Emoji found" + "Not in the current list of Discord Unicode Emojis and no Custom Emoji found" ) return argument @@ -88,7 +90,7 @@ async def convert(self, ctx, argument): if "emojitype" not in argument.lower(): raise commands.BadArgument("No `emojitype` flag") if len(argument) < 11: - raise commands.BadArgument("No argument specified for " "`emojitype` flag") + raise commands.BadArgument("No argument specified for `emojitype` flag") result = argument[10:].lower() if result not in ("all", "unicode", "custom", "here", "nothere", "score"): raise commands.BadArgument("Unknown emoji type specified for `emojitype` flag") @@ -102,7 +104,7 @@ async def convert(self, ctx, argument): if "emojiname" not in argument.lower(): raise commands.BadArgument("No `emojiname` flag") if len(argument) < 11: - raise commands.BadArgument("No argument specified for " "`emojiname` flag") + raise commands.BadArgument("No argument specified for `emojiname` flag") return f":{argument[10:]}:" @@ -129,7 +131,7 @@ async def convert(self, ctx, argument): try: return int(argument[7:]) except ValueError: - raise commands.BadArgument("`before` flag should take an " "integer as input") + raise commands.BadArgument("`before` flag should take an integer as input") class AfterConverter(commands.Converter): @@ -141,19 +143,20 @@ async def convert(self, ctx, argument): try: return int(argument[6:]) except ValueError: - raise commands.BadArgument("`after` flag should take an " "integer as input") + raise commands.BadArgument("`after` flag should take an integer as input") + +class Score(CanaryCog): + def __init__(self, bot: Canary): + super().__init__(bot) -class Score(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.guild = None - self.UPMARTLET = None - self.DOWNMARTLET = None + self.UPMARTLET: discord.Emoji | None = None + self.DOWNMARTLET: discord.Emoji | None = None @commands.Cog.listener() async def on_ready(self): - self.guild = self.bot.get_guild(self.bot.config.server_id) + await super().on_ready() + self.UPMARTLET = discord.utils.get(self.guild.emojis, name=self.bot.config.upvote_emoji) self.DOWNMARTLET = discord.utils.get(self.guild.emojis, name=self.bot.config.downvote_emoji) @@ -162,9 +165,9 @@ async def _get_converted_args_dict(ctx, args, from_xnor_to=False, from_nand_to=F # this will make an arg_converter with the possible values the # score, ranking, and emoji_ranking function can take, then do # additional checks to restrict it further - # If from_xnor_to, then if there is a from flag there must be a - # to flag and vice versa (used by score and emoji_ranking) - # If from_nand_to, there can either be a from or a to flag + # If from_xnor_to, then if there is a `from` flag there must be a + # `to` flag and vice versa (used by score and emoji_ranking) + # If from_nand_to, there can either be a `from` or a `to` flag # or nothing, not both (used by ranking) # If member, then a member can be input (will still look for # from/to flags but will raise an exception if both a member and @@ -266,16 +269,16 @@ def _where_str_and_values_from_args_dict(self, args_dict, prefix=None): elif args_dict["emojitype"] == "custom": where_list.append("instr(ReactionName, '<') = 1") elif args_dict["emojitype"] == "here": - where_list.append(f"ReactionName IN " f"({','.join(['?']*len(guild_emojis))})") + where_list.append(f"ReactionName IN ({','.join(['?']*len(guild_emojis))})") values_list = values_list + guild_emojis elif args_dict["emojitype"] == "nothere": # must be a custom react where_list.append("instr(ReactionName, '<') = 1") - where_list.append(f"ReactionName NOT IN " f"({','.join(['?']*len(guild_emojis))})") + where_list.append(f"ReactionName NOT IN ({','.join(['?']*len(guild_emojis))})") values_list = values_list + guild_emojis # elif args_dict["emojitype"] == "all", there are no restrictions # elif args_dict["emojitype"] == "score", this must be dealt with - # outside of this function as this does not use a where close + # outside this function as this does not use a where close if not args_dict["self"]: where_list.append("ReacterID != ReacteeID") if args_dict["before"]: @@ -307,41 +310,46 @@ async def _add_or_remove_reaction_from_db(self, payload, remove=False): except discord.errors.NotFound: return - conn = sqlite3.connect(self.bot.config.db_path) - conn.execute("PRAGMA foreign_keys = ON") - c = conn.cursor() + db: aiosqlite.Connection + async with self.db() as db: + reacter_id = self.bot.get_user(payload.user_id).id + await add_member_if_needed(self, db, reacter_id) + reactee_id = message.author.id + await add_member_if_needed(self, db, reactee_id) - reacter_id = self.bot.get_user(payload.user_id).id - await add_member_if_needed(self, c, reacter_id) - reactee_id = message.author.id - await add_member_if_needed(self, c, reactee_id) + emoji = payload.emoji - emoji = payload.emoji + if remove: + await db.execute( + "DELETE FROM Reactions WHERE ReacterID = ? AND ReacteeID = ? AND ReactionName = ? " + "AND MessageID = ?", + (reacter_id, reactee_id, str(emoji), message_id), + ) + else: + await db.execute( + "INSERT OR IGNORE INTO Reactions VALUES (?,?,?,?)", (reacter_id, reactee_id, str(emoji), message_id) + ) - if remove: - c.execute( - "DELETE FROM Reactions WHERE ReacterID = ? AND ReacteeID = ? " "AND ReactionName = ? AND MessageID = ?", - (reacter_id, reactee_id, str(emoji), message_id), - ) - else: - c.execute( - "INSERT OR IGNORE INTO Reactions VALUES (?,?,?,?)", (reacter_id, reactee_id, str(emoji), message_id) - ) - conn.commit() - conn.close() + await db.commit() - @commands.Cog.listener() + @CanaryCog.listener() async def on_raw_reaction_add(self, payload): + if self.guild is None: + return + if payload.guild_id == self.guild.id: await self._add_or_remove_reaction_from_db(payload) - @commands.Cog.listener() + @CanaryCog.listener() async def on_raw_reaction_remove(self, payload): + if self.guild is None: + return + if payload.guild_id == self.guild.id: await self._add_or_remove_reaction_from_db(payload, remove=True) @commands.command() - async def score(self, ctx, *args): + async def score(self, ctx: commands.Context, *args): """Display emoji score Basic examples: @@ -355,7 +363,8 @@ async def score(self, ctx, *args): -If @user or User, gives the score for this user. -If from and to flags, gives the score given from @userA to @userB. `from:all` and `to:all` can be used. -If nothing, gives your score - Note that it is possible to give usernames without mention. This is case sensitive. If a username contains a space, the username and flag must be included in quotes, e.g. "to:user name" + Note that it is possible to give usernames without mention. This is case-sensitive. If a username contains a + space, the username and flag must be included in quotes, e.g. "to:user name" - Optional: `emoji` OR `emojitype:type` OR `emojiname:name` OR `:name:` -If emoji, gives the score for this emoji @@ -381,32 +390,27 @@ async def score(self, ctx, *args): try: args_dict = await self._get_converted_args_dict(ctx, args, from_xnor_to=True) except commands.BadArgument as err: - await ctx.send(err) + await ctx.send(str(err)) return # get the WHERE conditions and the values where_str, t = self._where_str_and_values_from_args_dict(args_dict) - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() + if args_dict["emojitype"] != "score": - c.execute(f"SELECT count(ReacteeID) FROM Reactions " f"WHERE {where_str}", t) - react_count = c.fetchone()[0] + react_count_t = await self.fetch_one(f"SELECT count(ReacteeID) FROM Reactions WHERE {where_str}", t) else: - c.execute( + react_count_t = await self.fetch_one( ( - f"SELECT COUNT(IIF (ReactionName = ?1, 1, NULL)) - " - f"COUNT(IIF (ReactionName = ?2, 1, NULL)) " - f"FROM Reactions " - f"WHERE {where_str} " - f"AND (ReactionName = ?1 OR ReactionName=?2) " + f"SELECT COUNT(IIF (ReactionName = ?1, 1, NULL)) - COUNT(IIF (ReactionName = ?2, 1, NULL)) " + f"FROM Reactions WHERE {where_str} AND (ReactionName = ?1 OR ReactionName=?2) " ), (str(self.UPMARTLET), str(self.DOWNMARTLET), *t), ) - react_count = c.fetchone()[0] - await ctx.send(react_count) + if react_count_t is None: + return - conn.close() + await ctx.send(react_count_t[0]) @commands.command() async def ranking(self, ctx, *args): @@ -421,7 +425,8 @@ async def ranking(self, ctx, *args): -If from flag: gives the score received by every user from this user. `from:all` can be used. -If to flag: gives the score received by this user from every user. `to:all` can be used. -If nothing, gives the score of every user - Note that it is possible to give usernames without mention. This is case sensitive. If a username contains a space, the username and flag must be included in quotes, e.g. "to:user name" + Note that it is possible to give usernames without mention. This is case-sensitive. If a username contains a + space, the username and flag must be included in quotes, e.g. "to:user name" - Optional: `emoji` OR `emojitype:type` OR `emojiname:name` OR `:name:` -If emoji, gives the score for this emoji @@ -444,8 +449,6 @@ async def ranking(self, ctx, *args): -"nothere" (All custom emoji not in the server), -"score" (The emojis used as upvotes and downvotes) """ - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() try: args_dict = await self._get_converted_args_dict(ctx, args, from_nand_to=True, member=False) @@ -453,69 +456,56 @@ async def ranking(self, ctx, *args): await ctx.send(err) return - if not args_dict["to_member"]: - select_id = "ReacteeID" - else: - select_id = "ReacterID" + select_id = "ReacterID" if args_dict["to_member"] else "ReacteeID" + if args_dict["emojitype"] != "score": # get the WHERE conditions and the values where_str, t = self._where_str_and_values_from_args_dict(args_dict) - c.execute( - ( - f"SELECT printf('%d. %s', " - f"ROW_NUMBER() OVER (ORDER BY count(*) DESC), M.Name), " - f"printf('%d %s', count(*), " - f"IIF (count(*)!=1, 'times', 'time')) " - f"FROM Reactions AS R, Members as M " - f"WHERE {where_str} " - f"AND R.{select_id} = M.ID " - f"GROUP BY R.{select_id} " - f"ORDER BY count(*) DESC" - ), - t, + q = ( + f"SELECT printf('%d. %s', ROW_NUMBER() OVER (ORDER BY count(*) DESC), M.Name), " + f"printf('%d %s', count(*), " + f"IIF (count(*)!=1, 'times', 'time')) " + f"FROM Reactions AS R, Members as M " + f"WHERE {where_str} " + f"AND R.{select_id} = M.ID " + f"GROUP BY R.{select_id} " + f"ORDER BY count(*) DESC" ) - - counts = list(zip(*c.fetchall())) - if not counts: - await ctx.send(embed=discord.Embed(title="This reaction was never used on this server.")) - return - names, values = counts - + not_found_err = "This reaction was never used on this server." else: # get the WHERE conditions and the values - where_str, t = self._where_str_and_values_from_args_dict(args_dict, prefix="R") - c.execute( - ( - f"SELECT printf('%d. %s', " - f"ROW_NUMBER() OVER (ORDER BY TotalCount DESC), Name), " - f"TotalCount FROM " - f"(SELECT M.Name, " - f"COUNT(IIF (ReactionName = ?1, 1, NULL)) - " - f"COUNT(IIF (ReactionName = ?2, 1, NULL)) " - f"AS TotalCount " - f"FROM Reactions AS R, Members as M " - f"WHERE {where_str} " - f"AND (ReactionName = ?1 OR ReactionName=?2) " - f"AND R.{select_id} = M.ID " - f"GROUP BY R.{select_id})" - ), - (str(self.UPMARTLET), str(self.DOWNMARTLET), *t), + where_str, tp = self._where_str_and_values_from_args_dict(args_dict, prefix="R") + t = (str(self.UPMARTLET), str(self.DOWNMARTLET), *tp) + q = ( + f"SELECT printf('%d. %s', " + f"ROW_NUMBER() OVER (ORDER BY TotalCount DESC), Name), " + f"TotalCount FROM " + f"(SELECT M.Name, " + f"COUNT(IIF (ReactionName = ?1, 1, NULL)) - " + f"COUNT(IIF (ReactionName = ?2, 1, NULL)) " + f"AS TotalCount " + f"FROM Reactions AS R, Members as M " + f"WHERE {where_str} " + f"AND (ReactionName = ?1 OR ReactionName=?2) " + f"AND R.{select_id} = M.ID " + f"GROUP BY R.{select_id})" ) - counts = list(zip(*c.fetchall())) - if not counts: - await ctx.send(embed=discord.Embed(title="No results found")) - return - names = counts[0] - values = counts[1] - - conn.close() + not_found_err = "No results found" + + counts = list(zip(*(await self.fetch_list(q, t)))) + if not counts: + await ctx.send(embed=discord.Embed(title=not_found_err)) + return + + names, values = counts + paginator_dict = {"names": names, "values": values} p = Pages(ctx, item_list=paginator_dict, title="Score ranking", display_option=(2, 9), editable_content=False) await p.paginate() @commands.command() - async def emojiranking(self, ctx, *args): + async def emojiranking(self, ctx: commands.Context, *args): """Ranking of how many times emojis were used Basic example: @@ -525,7 +515,8 @@ async def emojiranking(self, ctx, *args): - Optional: `from:@userA` AND `to:@userB` -If from and to flags, gives the score given from @userA to @userB. `from:all` and `to:all` can be used. - Note that it is possible to give usernames without mention. If a username contains a space, the username and flag must be included in quotes, e.g. "to:user name" + Note that it is possible to give usernames without mention. If a username contains a space, the username and + flag must be included in quotes, e.g. "to:user name" - Optional: `emojitype:type` -If emojitype flag, gives the score for this emojitype (see below for types) @@ -545,40 +536,34 @@ async def emojiranking(self, ctx, *args): -"here" (All custom emojis in the server), -"nothere" (All custom emoji not in the server) """ - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() try: args_dict = await self._get_converted_args_dict(ctx, args, from_xnor_to=True, member=False, emoji=False) except commands.BadArgument as err: - await ctx.send(err) + await ctx.send(str(err)) return if args_dict["emojitype"] == "score": - await ctx.send("Invalid input: Emojitype flag cannot use " "type score for this function") + await ctx.send("Invalid input: Emojitype flag cannot use type score for this function") # get the WHERE conditions and the values where_str, t = self._where_str_and_values_from_args_dict(args_dict) - c.execute( - ( - f"SELECT printf('%d. %s', " - f"ROW_NUMBER() OVER (ORDER BY count(*) DESC), " - f"ReactionName), printf('%d %s', count(*), " - f"IIF (count(*)!=1, 'times', 'time')) " - f"FROM Reactions " - f"WHERE {where_str} " - f"GROUP BY ReactionName " - ), - t, + q = ( + f"SELECT printf('%d. %s', " + f"ROW_NUMBER() OVER (ORDER BY count(*) DESC), " + f"ReactionName), printf('%d %s', count(*), " + f"IIF (count(*)!=1, 'times', 'time')) " + f"FROM Reactions " + f"WHERE {where_str} " + f"GROUP BY ReactionName " ) - counts = list(zip(*c.fetchall())) + counts = list(zip(*(await self.fetch_list(q, t)))) if not counts: await ctx.send(embed=discord.Embed(title="No results found")) return - names = counts[0] - values = counts[1] - conn.close() + names, values = counts + paginator_dict = {"names": names, "values": values} p = Pages(ctx, item_list=paginator_dict, title="Emoji ranking", display_option=(2, 9), editable_content=False) diff --git a/cogs/subscribers.py b/canary/cogs/subscribers.py similarity index 86% rename from cogs/subscribers.py rename to canary/cogs/subscribers.py index 327993e19..a15718dde 100644 --- a/cogs/subscribers.py +++ b/canary/cogs/subscribers.py @@ -29,9 +29,13 @@ import re import pickle import feedparser -import requests + +# Type hinting +from ..bot import Canary +from .base_cog import CanaryCog # Subscriber decorator +from .utils.custom_requests import fetch from .utils.subscribers import canary_subscriber CFIA_FEED_URL = "http://inspection.gc.ca/eng/1388422350443/1388422374046.xml" @@ -58,9 +62,9 @@ os.makedirs("./data/runtime", exist_ok=True) -class Subscribers(commands.Cog): - def __init__(self, bot): - self.bot = bot +class Subscribers(CanaryCog): + def __init__(self, bot: Canary): + super().__init__(bot) # Compiled recall regular expression for filtering self._recall_filter = re.compile(self.bot.config.recall_filter, re.IGNORECASE) @@ -73,18 +77,18 @@ def __init__(self, bot): METRO_BLUE_LINE: METRO_NORMAL_SERVICE_MESSAGE, } - self._recall_channel = None - self._metro_status_channel = None + self._recall_channel: discord.TextChannel | None = None + self._metro_status_channel: discord.TextChannel | None = None @commands.Cog.listener() async def on_ready(self): - self._recall_channel = utils.get( - self.bot.get_guild(self.bot.config.server_id).text_channels, name=self.bot.config.recall_channel - ) + await super().on_ready() + + if not self.guild: + return - self._metro_status_channel = utils.get( - self.bot.get_guild(self.bot.config.server_id).text_channels, name=self.bot.config.metro_status_channel - ) + self._recall_channel = utils.get(self.guild.text_channels, name=self.bot.config.recall_channel) + self._metro_status_channel = utils.get(self.guild.text_channels, name=self.bot.config.metro_status_channel) # Register all subscribers self.bot.loop.create_task(self.cfia_rss()) @@ -98,6 +102,9 @@ async def cfia_rss(self): feed for updates. """ + if not self._recall_channel: + return + newest_recalls = feedparser.parse(CFIA_FEED_URL)["entries"] try: @@ -117,7 +124,7 @@ async def cfia_rss(self): new_recalls = True recalls[recall_id] = "" recall_warning = discord.Embed(title=recall["title"], description=recall["link"]) - soup = BeautifulSoup(recall["summary"], "html.parser") + soup = BeautifulSoup(recall["summary"], "lxml") try: img_url = soup.img["src"] @@ -155,9 +162,11 @@ async def metro_status(self): outages. """ + if not self._metro_status_channel: + return + try: - response = requests.get(METRO_STATUS_API) - response_data = response.json() + response_data = await fetch(METRO_STATUS_API, "json") except json.decoder.JSONDecodeError: # STM API sometimes returns non-JSON responses return diff --git a/canary/cogs/utils/__init__.py b/canary/cogs/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cogs/utils/arg_converter.py b/canary/cogs/utils/arg_converter.py similarity index 97% rename from cogs/utils/arg_converter.py rename to canary/cogs/utils/arg_converter.py index a002e5190..f94d83db1 100644 --- a/cogs/utils/arg_converter.py +++ b/canary/cogs/utils/arg_converter.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -140,7 +140,7 @@ async def convert(self, ctx, arguments): # values or raise commands.BadArgument if they are required for key in remaining_vars: if len(self._converters_dict[key]) == 1: - raise commands.BadArgument(f"Invalid input: Missing required " f"argument {key}") + raise commands.BadArgument(f"Invalid input: Missing required argument {key}") else: converted_arguments_dict[key] = self._converters_dict[key][1] return converted_arguments_dict @@ -149,7 +149,7 @@ async def convert(self, ctx, arguments): # Default converters class IntConverter(commands.Converter): @staticmethod - async def convert(ctx, argument): + async def convert(ctx, argument, **kwargs): try: return int(argument) except ValueError: @@ -158,7 +158,7 @@ async def convert(ctx, argument): class StrConverter(commands.Converter): @staticmethod - async def convert(ctx, argument): + async def convert(ctx, argument, **kwargs): try: return str(argument) except ValueError: diff --git a/cogs/utils/auto_incorrect.py b/canary/cogs/utils/auto_incorrect.py similarity index 89% rename from cogs/utils/auto_incorrect.py rename to canary/cogs/utils/auto_incorrect.py index 410f9f3bb..ba9862c2e 100644 --- a/cogs/utils/auto_incorrect.py +++ b/canary/cogs/utils/auto_incorrect.py @@ -3,7 +3,7 @@ __all__ = ["auto_incorrect"] -def _swap(s, i): +def _swap(s: str, i: int): """ Given string s and index i, it swaps s[i] and s[i+1] and returns By @lazho @@ -14,14 +14,14 @@ def _swap(s, i): return s -def _repeat(s, i): +def _repeat(s: str, i: int): """ By @lazho """ return s[:i] + s[i] + s[i:] -def _omit(s, i): +def _omit(s: str, i: int): """ Given string s and index i, it omits the i-th character. By @lazho @@ -29,7 +29,7 @@ def _omit(s, i): return s[:i] + s[i + 1 :] -REPLACE_CASES = { +REPLACE_CASES: dict[str, str] = { "your": "you're", "you're": "your", "its": "it's", @@ -58,16 +58,16 @@ def _omit(s, i): } -def auto_incorrect(input_str): +def auto_incorrect(input_str: str): """ By @lazho """ chance = 99 - input_str = input_str.split() + input_split: list[str] = input_str.split() output_str = "" - for w in input_str: + for w in input_split: output_str += " " # Idiots don't use shift or capslock diff --git a/cogs/utils/checks.py b/canary/cogs/utils/checks.py similarity index 67% rename from cogs/utils/checks.py rename to canary/cogs/utils/checks.py index dabde3ffe..563fb5b19 100644 --- a/cogs/utils/checks.py +++ b/canary/cogs/utils/checks.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2019) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -21,16 +18,15 @@ import discord from discord.ext import commands -from bot import moderator_role, developer_role +from canary.bot import config def is_moderator(): """Returns True if user has a moderator role, raises an exception otherwise""" - def predicate(ctx): - role = discord.utils.get(ctx.author.roles, name=moderator_role) - if role is None: - raise commands.MissingPermissions([moderator_role]) + def predicate(ctx: commands.Context): + if discord.utils.get(ctx.author.roles, name=config.moderator_role) is None: + raise commands.MissingPermissions([config.moderator_role]) return True return commands.check(predicate) @@ -39,10 +35,9 @@ def predicate(ctx): def is_developer(): """Returns True if user is a bot developer, raises an exception otherwise""" - def predicate(ctx): - role = discord.utils.get(ctx.author.roles, name=developer_role) - if role is None: - raise commands.MissingPermissions([developer_role]) + def predicate(ctx: commands.Context): + if discord.utils.get(ctx.author.roles, name=config.developer_role) is None: + raise commands.MissingPermissions([config.developer_role]) return True return commands.check(predicate) diff --git a/cogs/utils/clamp_default.py b/canary/cogs/utils/clamp_default.py similarity index 86% rename from cogs/utils/clamp_default.py rename to canary/cogs/utils/clamp_default.py index ae7d228a1..8bbfeae19 100644 --- a/cogs/utils/clamp_default.py +++ b/canary/cogs/utils/clamp_default.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2019) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -17,8 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . +from typing import Any + -def clamp_default(val, min_val, max_val, default): +def clamp_default(val: str | int | None, min_val: int, max_val: int, default: Any): """ Enforces a minimum and maximum (closed) bound on an integer. Returns a default value if val is not an integer or false-y. diff --git a/cogs/utils/custom_requests.py b/canary/cogs/utils/custom_requests.py similarity index 90% rename from cogs/utils/custom_requests.py rename to canary/cogs/utils/custom_requests.py index 856268b56..ded2565c5 100644 --- a/cogs/utils/custom_requests.py +++ b/canary/cogs/utils/custom_requests.py @@ -44,6 +44,7 @@ async def fetch(url, type="content"): if type.lower() == "content": return await response.text() elif type.lower() == "json": - return await response.json() + # Disable content type checking since STM sucks and returns json as text/html + return await response.json(content_type=None) else: raise InvalidTypeException diff --git a/cogs/utils/dice_roll.py b/canary/cogs/utils/dice_roll.py similarity index 89% rename from cogs/utils/dice_roll.py rename to canary/cogs/utils/dice_roll.py index 90720baa8..5afd5ac49 100644 --- a/cogs/utils/dice_roll.py +++ b/canary/cogs/utils/dice_roll.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2019) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -20,7 +18,7 @@ from random import randint -def dice_roll(sides=20, n=1, modifier=0, mpr=False): +def dice_roll(sides: int = 20, n: int = 1, modifier: int = 0, mpr: bool = False): """ Rolls a die with given parameters Set mpr to True to mod each roll, otherwise, only the sum is modified diff --git a/cogs/utils/hangman.py b/canary/cogs/utils/hangman.py similarity index 90% rename from cogs/utils/hangman.py rename to canary/cogs/utils/hangman.py index d1223ca01..621b6a6ae 100644 --- a/cogs/utils/hangman.py +++ b/canary/cogs/utils/hangman.py @@ -1,4 +1,5 @@ -# Copyright (C) idoneam (2016-2021) +#!/usr/bin/env python3 +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -77,7 +78,7 @@ def mk_animal_list() -> list[tuple[str, str]]: animal_list_soup = BeautifulSoup( - requests.get("https://en.wikipedia.org/wiki/List_of_animal_names").content, "html.parser" + requests.get("https://en.wikipedia.org/wiki/List_of_animal_names").content, "lxml" ).find_all("tr") animal_list: list[tuple[str, str]] = [] for i in range(16, len(animal_list_soup)): @@ -85,9 +86,7 @@ def mk_animal_list() -> list[tuple[str, str]]: if curr_entry is None: continue animal_name = curr_entry.find("a") - animal_soup = BeautifulSoup( - requests.get(f"https://en.wikipedia.org{animal_name['href']}").content, "html.parser" - ) + animal_soup = BeautifulSoup(requests.get(f"https://en.wikipedia.org{animal_name['href']}").content, "lxml") img_list = animal_soup.find_all("img") img_index = 0 while str(img_list[img_index]["src"]).endswith(".svg.png"): @@ -98,7 +97,7 @@ def mk_animal_list() -> list[tuple[str, str]]: def mk_country_list() -> list[tuple[str, str]]: elem_list_soup = ( - BeautifulSoup(requests.get("https://en.wikipedia.org/wiki/List_of_sovereign_states").content, "html.parser") + BeautifulSoup(requests.get("https://en.wikipedia.org/wiki/List_of_sovereign_states").content, "lxml") .find("table", {"class": "sortable wikitable"}) .find_all("tr") ) @@ -116,9 +115,7 @@ def mk_country_list() -> list[tuple[str, str]]: ( country_name, "https:" - + BeautifulSoup( - requests.get(f"https://en.wikipedia.org{country_name_entry['href']}").content, "html.parser" - ) + + BeautifulSoup(requests.get(f"https://en.wikipedia.org{country_name_entry['href']}").content, "lxml") .find("table", {"class": "infobox"}) .find("a", {"class": "image"}) .find("img")["src"], @@ -129,7 +126,7 @@ def mk_country_list() -> list[tuple[str, str]]: def mk_element_list() -> list[tuple[str, Optional[str]]]: elem_list_soup = BeautifulSoup( - requests.get("https://en.wikipedia.org/wiki/List_of_chemical_elements").content, "html.parser" + requests.get("https://en.wikipedia.org/wiki/List_of_chemical_elements").content, "lxml" ).find_all("tr") elem_list: list[tuple[str, Optional[str]]] = [] for i in range(4, 118): @@ -138,9 +135,7 @@ def mk_element_list() -> list[tuple[str, Optional[str]]]: try: elem_img: Optional[str] = ( "https:" - + BeautifulSoup( - requests.get(f"https://en.wikipedia.org{elem_name_entry['href']}").content, "html.parser" - ) + + BeautifulSoup(requests.get(f"https://en.wikipedia.org{elem_name_entry['href']}").content, "lxml") .find("table", {"class": "infobox"}) .find("a") .find("img")["src"] @@ -153,7 +148,7 @@ def mk_element_list() -> list[tuple[str, Optional[str]]]: def mk_movie_list() -> list[tuple[str, str]]: kino_elem_soup = BeautifulSoup( - requests.get("https://en.wikipedia.org/wiki/List_of_years_in_film").content, "html.parser" + requests.get("https://en.wikipedia.org/wiki/List_of_years_in_film").content, "lxml" ).find_all("i") kino_list: list[tuple[str, str]] = [] for i in range(195, len(kino_elem_soup)): @@ -162,7 +157,7 @@ def mk_movie_list() -> list[tuple[str, str]]: try: kino_img = ( "https:" - + BeautifulSoup(requests.get(f"https://en.wikipedia.org{kino_elem['href']}").content, "html.parser") + + BeautifulSoup(requests.get(f"https://en.wikipedia.org{kino_elem['href']}").content, "lxml") .find("table", {"class": "infobox"}) .find("a", {"class": "image"}) .find("img")["src"] diff --git a/cogs/utils/image_helpers.py b/canary/cogs/utils/image_helpers.py similarity index 99% rename from cogs/utils/image_helpers.py rename to canary/cogs/utils/image_helpers.py index c04c2f7bf..e2040e4f3 100644 --- a/cogs/utils/image_helpers.py +++ b/canary/cogs/utils/image_helpers.py @@ -29,7 +29,6 @@ def apply_transform(transform, buffer, size, max_size, ext, is_png, *args): async def filter_image(loop, transform, ctx, history_limit, max_size, *args): - att = await get_attachment(ctx, history_limit) if att is None: await ctx.send( diff --git a/cogs/utils/members.py b/canary/cogs/utils/members.py similarity index 73% rename from cogs/utils/members.py rename to canary/cogs/utils/members.py index 67e2d9ab8..f9babbcc2 100644 --- a/cogs/utils/members.py +++ b/canary/cogs/utils/members.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . +import aiosqlite import discord @@ -32,8 +33,9 @@ async def _get_name_from_id(self, user_id) -> str: return str(user_id) -async def add_member_if_needed(self, c, user_id) -> None: - c.execute("SELECT Name FROM Members WHERE ID = ?", (user_id,)) - if not c.fetchone(): - name = await _get_name_from_id(self, user_id) - c.execute("INSERT OR IGNORE INTO Members VALUES (?,?)", (user_id, name)) +async def add_member_if_needed(self, db: aiosqlite.Connection, user_id) -> None: + c: aiosqlite.Cursor + async with db.execute("SELECT Name FROM Members WHERE ID = ?", (user_id,)) as c: + if not (await c.fetchone()): + name = await _get_name_from_id(self, user_id) + await db.execute("INSERT OR IGNORE INTO Members VALUES (?,?)", (user_id, name)) diff --git a/cogs/utils/mock_context.py b/canary/cogs/utils/mock_context.py similarity index 54% rename from cogs/utils/mock_context.py rename to canary/cogs/utils/mock_context.py index d53f1aa4b..0dbc16f78 100644 --- a/cogs/utils/mock_context.py +++ b/canary/cogs/utils/mock_context.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -16,7 +16,6 @@ # along with Canary. If not, see . from dataclasses import dataclass -from typing import Union, Optional from discord import User, Member, Guild, ClientUser, Message, VoiceProtocol from discord.ext.commands import Bot, Cog, Command from discord.abc import Messageable @@ -26,21 +25,21 @@ class MockContext: """Class that can be used to mock a discord context""" - args: Optional[list] = None - author: Optional[Union[User, Member]] = None - bot: Optional[Bot] = None - channel: Optional[Union[Messageable]] = None - cog: Optional[Cog] = None - command: Optional[Command] = None - command_failed: Optional[bool] = None - guild: Optional[Guild] = None - invoked_parents: Optional[list[str]] = None - invoked_subcommand: Optional[Command] = None - invoked_with: Optional[str] = None - kwargs: Optional[dict] = None - me: Optional[Union[Member, ClientUser]] = None - message: Optional[Message] = None - prefix: Optional[str] = None - subcommand_passed: Optional[str] = None - valid: Optional[bool] = None - voice_client: Optional[VoiceProtocol] = None + args: list | None = None + author: User | Member | None = None + bot: Bot | None = None + channel: Messageable | None = None + cog: Cog | None = None + command: Command | None = None + command_failed: bool | None = None + guild: Guild | None = None + invoked_parents: list[str] | None = None + invoked_subcommand: Command | None = None + invoked_with: str | None = None + kwargs: dict | None = None + me: Member | ClientUser | None = None + message: Message | None = None + prefix: str | None = None + subcommand_passed: str | None = None + valid: bool | None = None + voice_client: VoiceProtocol | None = None diff --git a/cogs/utils/music_helpers.py b/canary/cogs/utils/music_helpers.py similarity index 82% rename from cogs/utils/music_helpers.py rename to canary/cogs/utils/music_helpers.py index fb5e79a51..f3fa86d4f 100644 --- a/cogs/utils/music_helpers.py +++ b/canary/cogs/utils/music_helpers.py @@ -1,9 +1,27 @@ +# Copyright (C) idoneam (2016-2023) +# +# This file is part of Canary +# +# Canary is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Canary is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Canary. If not, see . + import time from functools import wraps from typing import Iterable import yt_dlp import discord import random +from discord.ext.commands import Context class MusicArgConvertError(ValueError): @@ -54,12 +72,12 @@ def insert_converter(arg: str): idx, url = arg.split(maxsplit=1) except ValueError as e: raise MusicArgConvertError(e) - return (int(idx), url) + return int(idx), url def check_playing(func): @wraps(func) - async def wrapper(self, ctx, *args, **kwargs): + async def wrapper(self, ctx: Context, *args, **kwargs): if self.playing is None or ctx.voice_client is None or (not self.track_lock.locked()): await ctx.send("bot is not currently playing anything to a voice channel.") elif (ctx.author.voice is None or ctx.author.voice.channel != ctx.voice_client.channel) and len( @@ -72,7 +90,7 @@ async def wrapper(self, ctx, *args, **kwargs): return wrapper -def mk_title_string(inf_dict) -> str: +def mk_title_string(inf_dict: dict) -> str: url = inf_dict.get("webpage_url") return ( inf_dict.get("title", "title not found") @@ -111,7 +129,7 @@ def parse_time(time_str: str) -> int: def time_func(func): - async def wrapper(self, ctx, time_str: str): + async def wrapper(self, ctx: Context, time_str: str): try: parsed = parse_time(time_str) except ValueError: @@ -145,7 +163,7 @@ def mk_change_embed(data, track_list, title_str: str, footer_str: str) -> discor def check_banned(func): @wraps(func) - async def wrapper(self, ctx, *args, **kwargs): + async def wrapper(self, ctx: Context, *args, **kwargs): if discord.utils.get(ctx.author.roles, name=self.ban_role): return await ctx.send(f"you have the role `{self.ban_role}`, you are not allowed to do this.") return await func(self, ctx, *args, **kwargs) diff --git a/cogs/utils/p_strings.py b/canary/cogs/utils/p_strings.py similarity index 96% rename from cogs/utils/p_strings.py rename to canary/cogs/utils/p_strings.py index 8de6aac44..289420cab 100644 --- a/cogs/utils/p_strings.py +++ b/canary/cogs/utils/p_strings.py @@ -54,7 +54,7 @@ PLACEHOLDERS_PATTERNS = (re.compile(f"%{arg}%") for arg in PLACEHOLDERS_ARGS) -def _convert_choice_list(choice_list_string, to_pattern_str=False): +def _convert_choice_list(choice_list_string: str, to_pattern_str: bool = False): """ Takes a string with choice list placeholders and converts them to either a string where the choices are made (default) @@ -118,7 +118,7 @@ def _get_pattern_from_string(string, anywhere=False): class PString: - def __init__(self, string, user=None, channel=None, groups=[], additional_info=None): + def __init__(self, string, user=None, channel=None, groups: list | None = None, additional_info=None): """ A p-string is composed of a string with placeholders in it, and values for these placeholders. Printing a p-string will @@ -151,7 +151,7 @@ def __init__(self, string, user=None, channel=None, groups=[], additional_info=N self.string = string self.user = user self.channel = channel - self.groups = groups + self.groups: list = groups or [] self.additional_info = additional_info @property @@ -169,7 +169,13 @@ def __str__(self): class PStringEncodings: - def __init__(self, input_strings, output_strings, anywhere_values, additional_info_list=None): + def __init__( + self, + input_strings: list[str], + output_strings: list[str], + anywhere_values: bool | list[bool], + additional_info_list: list | None = None, + ): """ Used to encode a list of input strings with placeholders and a list of output strings with placeholders to @@ -233,7 +239,7 @@ def __init__(self, input_strings, output_strings, anywhere_values, additional_in for output_string, pattern, additional_info in zip(output_strings, self.patterns, additional_info_list) ] - def parser(self, content, user=None, channel=None): + def parser(self, content: str, user=None, channel=None): """ Return either None if the content matches no input pattern, or a random corresponding filled output p-string if it matches some diff --git a/cogs/utils/paginator.py b/canary/cogs/utils/paginator.py similarity index 95% rename from cogs/utils/paginator.py rename to canary/cogs/utils/paginator.py index ad316c8a9..46dc42e44 100644 --- a/cogs/utils/paginator.py +++ b/canary/cogs/utils/paginator.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -26,15 +24,15 @@ class Pages: def __init__( self, ctx, - current_page=1, - msg=None, + current_page: int = 1, + msg: discord.Message | None = None, item_list=[], - title="Paginator", + title: str = "Paginator", display_option=(1, 0), - editable_content=True, - editable_content_emoji="🚮", - return_user_on_edit=False, - timeout=300, + editable_content: bool = True, + editable_content_emoji: str = "🚮", + return_user_on_edit: bool = False, + timeout: int = 300, ): """Creates a paginator. @@ -98,7 +96,7 @@ def __init__( self.guild = ctx.guild self.channel = ctx.channel self.user = ctx.author - self.message = msg + self.message: discord.Message | None = msg self.itemList = item_list self.title = title self.displayOption = display_option @@ -113,7 +111,7 @@ def __init__( if editable_content: self.actions.append((editable_content_emoji, self._edit)) self.currentPage = current_page - self.edit_mode = False + self.edit_mode: bool = False self.return_user_on_edit = return_user_on_edit self.timeout = timeout @@ -213,7 +211,7 @@ def _organize_embeds_list_embeds(self, pages_to_send): async def _show_page(self, page): self.currentPage = max(0, min(page, self.lastPage)) - if self.message: + if self.message is not None: if self.currentPage == 0: try: await self.message.delete() @@ -236,7 +234,7 @@ async def _show_page(self, page): self.message = await self.channel.send( embed=self.pagesToSend[self.currentPage], delete_after=self.timeout ) - for (emoji, _) in self.actions: + for emoji, _ in self.actions: await self.message.add_reaction(emoji) return @@ -264,7 +262,7 @@ def _react_check(self, reaction, user): return False if reaction.message.id != self.message.id: return False - for (emoji, action) in self.actions: + for emoji, action in self.actions: if reaction.emoji != emoji: continue self.user = user diff --git a/cogs/utils/role_restoration.py b/canary/cogs/utils/role_restoration.py similarity index 57% rename from cogs/utils/role_restoration.py rename to canary/cogs/utils/role_restoration.py index 8cd4d0dea..edc5f5c44 100644 --- a/cogs/utils/role_restoration.py +++ b/canary/cogs/utils/role_restoration.py @@ -1,4 +1,4 @@ -# Copyright (C) idoneam (2016-2022) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -15,101 +15,101 @@ # You should have received a copy of the GNU General Public License # along with Canary. If not, see . +import aiosqlite import discord -import sqlite3 from discord import utils from discord.ext import commands from .paginator import Pages from .mock_context import MockContext -from bot import muted_role as muted_role_name +from canary.bot import Canary import datetime -def save_existing_roles(bot, user: discord.Member, muted: bool = False, appeal_channel: discord.TextChannel = None): - roles_id = [role.id for role in user.roles if role.name not in ("@everyone", muted_role_name)] +async def save_existing_roles( + bot: Canary, user: discord.Member, muted: bool = False, appeal_channel: discord.TextChannel | None = None +): + roles_id = [role.id for role in user.roles if role.name not in ("@everyone", bot.config.muted_role)] if not roles_id and not muted: return - conn = sqlite3.connect(bot.config.db_path) - try: - c = conn.cursor() + db: aiosqlite.Connection + async with bot.db() as db: # store roles as a string of IDs separated by spaces if muted: - now = datetime.datetime.now() - if is_in_muted_table(bot, user): - t = (appeal_channel.id, user.id) - c.execute(f"UPDATE MutedUsers SET AppealChannelID = ? WHERE UserID = ?", t) + if not appeal_channel: + return + + if await is_in_muted_table(bot, user): + await db.execute( + f"UPDATE MutedUsers SET AppealChannelID = ? WHERE UserID = ?", (appeal_channel.id, user.id) + ) else: - t = (user.id, appeal_channel.id, " ".join(str(e) for e in roles_id), now) - c.execute(f"REPLACE INTO MutedUsers VALUES (?, ?, ?, ?)", t) + await db.execute( + f"REPLACE INTO MutedUsers VALUES (?, ?, ?, ?)", + (user.id, appeal_channel.id, " ".join(str(e) for e in roles_id), datetime.datetime.now()), + ) + else: - t = (user.id, " ".join(str(e) for e in roles_id)) - c.execute(f"REPLACE INTO PreviousRoles VALUES (?, ?)", t) - conn.commit() - finally: - conn.close() + await db.execute(f"REPLACE INTO PreviousRoles VALUES (?, ?)", (user.id, " ".join(str(e) for e in roles_id))) + await db.commit() -def fetch_saved_roles(bot, guild, user: discord.Member, muted: bool = False) -> list[discord.Role] | None: - conn = sqlite3.connect(bot.config.db_path) - try: - c = conn.cursor() - if muted: - fetched_roles = c.execute(f"SELECT Roles FROM MutedUsers WHERE UserID = ?", (user.id,)).fetchone() - else: - fetched_roles = c.execute(f"SELECT Roles FROM PreviousRoles WHERE ID = ?", (user.id,)).fetchone() - # the above returns a tuple with a string of IDs separated by spaces - - # Return list of all valid roles restored from the DB - # - filter(None, ...) strips false-y elements - return ( - list( - filter(None, (guild.get_role(int(role_id)) for role_id in fetched_roles[0].split(" ") if role_id != "")) - ) - if fetched_roles - else None - ) - finally: - conn.close() +async def fetch_saved_roles(bot: Canary, guild, user: discord.Member, muted: bool = False) -> list[discord.Role] | None: + db: aiosqlite.Connection + c: aiosqlite.Cursor + async with bot.db() as db: + q = f"SELECT Roles FROM {'MutedUsers WHERE UserID' if muted else 'PreviousRoles WHERE ID'} = ?" + async with db.execute(q, (user.id,)) as c: + fetched_roles = await c.fetchone() -def has_muted_role(user: discord.Member): - muted_role = utils.get(user.guild.roles, name=muted_role_name) + # the above returns a tuple with a string of IDs separated by spaces + + # Return list of all valid roles restored from the DB + # - filter(None, ...) strips false-y elements + return ( + list(filter(None, (guild.get_role(int(role_id)) for role_id in fetched_roles[0].split(" ") if role_id != ""))) + if fetched_roles + else None + ) + + +def has_muted_role(bot: Canary, user: discord.Member): + muted_role = utils.get(user.guild.roles, name=bot.config.muted_role) return muted_role and next((r for r in user.roles if r == muted_role), None) is not None -def is_in_muted_table(bot, user: discord.Member): - conn = sqlite3.connect(bot.config.db_path) - try: - c = conn.cursor() - muted = c.execute("SELECT * FROM MutedUsers WHERE UserID = ?", (user.id,)).fetchone() - return muted is not None - finally: - conn.close() +async def is_in_muted_table(bot: Canary, user: discord.Member): + db: aiosqlite.Connection + c: aiosqlite.Cursor + async with bot.db() as db: + async with db.execute("SELECT * FROM MutedUsers WHERE UserID = ?", (user.id,)) as c: + return (await c.fetchone()) is not None -def remove_from_muted_table(bot, user: discord.Member): - conn = sqlite3.connect(bot.config.db_path) - try: - c = conn.cursor() - c.execute("DELETE FROM MutedUsers WHERE UserID = ?", (user.id,)) - conn.commit() - finally: - conn.close() +async def remove_from_muted_table(bot: Canary, user: discord.Member): + db: aiosqlite.Connection + async with bot.db() as db: + await db.execute("DELETE FROM MutedUsers WHERE UserID = ?", (user.id,)) + await db.commit() async def role_restoring_page( - bot, + bot: Canary, ctx: discord.ext.commands.Context | MockContext, user: discord.Member, roles: list[discord.Role] | None, muted: bool = False, ): - channel = ctx.channel + channel: discord.TextChannel | None = ctx.channel # Can be None from MockContext + + if channel is None: + return + if roles is None: # No row found in DB, as opposed to empty list if not muted: # don't say anything if this is while unmuting @@ -168,7 +168,7 @@ async def role_restoring_page( ) embed = discord.Embed( - title=f"{user.display_name}'s previous roles were " f"successfully added back by {ok_user.display_name}" + title=f"{user.display_name}'s previous roles were successfully added back by {ok_user.display_name}" ) await message.edit(embed=embed) await message.clear_reaction("◀") diff --git a/cogs/utils/site_save.py b/canary/cogs/utils/site_save.py similarity index 96% rename from cogs/utils/site_save.py rename to canary/cogs/utils/site_save.py index 8dfabeafb..e9196dd4d 100644 --- a/cogs/utils/site_save.py +++ b/canary/cogs/utils/site_save.py @@ -1,6 +1,5 @@ import os from time import time -from typing import Optional import discord from .custom_requests import fetch from functools import wraps diff --git a/cogs/utils/subscribers.py b/canary/cogs/utils/subscribers.py similarity index 100% rename from cogs/utils/subscribers.py rename to canary/cogs/utils/subscribers.py diff --git a/canary/config/__init__.py b/canary/config/__init__.py new file mode 100644 index 000000000..9e6f2f6c8 --- /dev/null +++ b/canary/config/__init__.py @@ -0,0 +1,5 @@ +__all__ = [ + "Config", +] + +from .config import Config diff --git a/canary/config/config.py b/canary/config/config.py new file mode 100644 index 000000000..57d241ba7 --- /dev/null +++ b/canary/config/config.py @@ -0,0 +1,250 @@ +# Copyright (C) idoneam (2016-2023) +# +# This file is part of Canary +# +# Canary is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Canary is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Canary. If not, see . + +from pathlib import Path +from pydantic import BaseModel, BaseSettings + +from typing import Literal + +__all__ = [ + "CurrencyModel", + "MusicModel", + "Config", +] + + +class CurrencyModel(BaseModel): + name: str = "cheeps" + symbol: str = "ʧ" + precision: int = 2 + initial: int = 1000 + + bet_roll_cases: tuple[int, ...] = (66, 90, 99, 100) # d100 roll threshold + bet_roll_returns: tuple[int, ...] = (0, 2, 4, 10) # multiplication factorys + + # Unused old ideas laid to rest below + # "salary_base": decimal.Decimal(config["Currency"]["SalaryBase"]), + # "inflation": decimal.Decimal(config["Currency"]["Inflation"]), + # "income_tax": {decimal.Decimal(b): float(a) for b, a in income_tb}, + # "asset_tax": {decimal.Decimal(b): float(a) for b, a in asset_tb}, + # "transaction_tax": float(config["OtherTax"]["TransactionTax"]), + + +class MusicModel(BaseModel): + ban_role: str = "tone deaf" + start_vol: float = 100.0 + + +class ImagesModel(BaseModel): + max_image_size: int = 8000000 + image_history_limit: int = 50 + max_radius: int = 500 + max_iterations: int = 20 + + +class GamesModel(BaseModel): + hm_norm_win: int = 10 + hm_cool_win: int = 20 + hm_timeout: int = 600 + + +class RolesModel(BaseModel): + pronouns: tuple[str, ...] = ("She/Her", "He/Him", "They/Them") + fields: tuple[str, ...] = ( + "Accounting", + "Agriculture", + "Anatomy and Cell Biology", + "Anthropology", + "Architecture", + "Biochemistry", + "Bioengineering", + "Biology", + "Bioresource Engineering", + "Chemical Engineering", + "Chemistry", + "Civil Engineering", + "Classics", + "cogito", + "Commerce", + "Computer Engineering", + "Computer Science", + "Computer Science/Biology", + "Cultural Studies", + "Desautels", + "Economics", + "Electrical Engineering", + "English", + "Experimental Medicine", + "Finance", + "Geography", + "History", + "Human Genetics", + "Indigenous Studies", + "International Development Studies", + "Jewish Studies", + "linguini", + "mac kid", + "Materials Engineering", + "Math", + "MBA", + "Mechanical Engineering", + "Medicine", + "Microbiology and Immunology", + "Neuroscience", + "Nursing", + "Pharmacology", + "Philosophy", + "Physical Therapy", + "Physics", + "Physiology", + "Political Science", + "Psychiatry", + "Psychology", + "Public Health", + "Social Work", + "Sociology", + "Software Engineering", + "Statistics", + "Theology", + "Urban Systems", + ) + faculties: tuple[str, ...] = ( + "Science", + "Engineering", + "Management", + "art you glad you're not in arts", + "ArtSci", + "Agriculture and Environment", + "Continuing Studies", + "Law", + "Education", + "Dentistry", + "Music", + ) + years: tuple[str, ...] = ("U0", "U1", "U2", "U3", "U4", "grad student", "workhere", "wenthere") + generics: tuple[str, ...] = ( + "weeb", + "weeb stomper", + "crosswords", + "stm_alertee", + "Stardew", + "R6", + "CS:GO Popflash", + "CS:GO Comp", + "Minecraft", + "Among Us", + "Pokemon Go", + "Secret Crabbo", + "Warzone", + "Monster Hunter", + "undersad", + ) + + +class Config(BaseSettings): + # Logging + log_level: Literal["critical", "error", "warning", "info", "debug", "notset"] = "info" + log_file: Path = Path.cwd() / "canary.log" + + dev_log_webhook_id: int | None = None + dev_log_webhook_token: str | None = None + mod_log_webhook_id: int | None = None + mod_log_webhook_token: str | None = None + + # Discord token + discord_key: str + + # Server configs + server_id: int + command_prefix: str = "?" + bot_name: str = "Marty" + + # Emoji + upvote_emoji: str = "upmartlet" + downvote_emoji: str = "downmartlet" + banner_vote_emoji: str = "redchiken" + + # Roles + moderator_role: str = "Discord Moderator" + developer_role: str = "idoneam" + mcgillian_role: str = "McGillian" + honorary_mcgillian_role: str = "Honorary McGillian" + banner_reminders_role: str = "Banner Submissions" + banner_winner_role: str = "Banner of the Week Winner" + trash_tier_banner_role: str = "Trash Tier Banner Submissions" + no_food_spotting_role: str = "Trash Tier Foodspotting" + muted_role: str = "Muted" + crabbo_role: str = "Secret Crabbo" + + # Channels + reception_channel: str = "martys_dm" + banner_of_the_week_channel: str = "banner_of_the_week" + banner_submissions_channel: str = "banner_submissions" + banner_converted_channel: str = "converted_banner_submissions" + food_spotting_channel: str = "foodspotting" + metro_status_channel: str = "stm_alerts" + bots_channel: str = "bots" + verification_channel: str = "verification_log" + appeals_log_channel: str = "appeals_log" + appeals_category: str = "appeals" + + # Meta + repository: str = "https://github.com/idoneam/Canary.git" + + # Welcome + Farewell messages + # NOT PORTED FROM OLD CONFIG SETUP. + # self.welcome = config["Greetings"]["Welcome"].split("\n") + # self.goodbye = config["Greetings"]["Goodbye"].split("\n") + + # DB configuration + db_path: str = "./data/runtime/Martlet.db" + + # Helpers configuration + course_year_range: str = "2024-2025" + course_tpl: str = "http://www.mcgill.ca/study/{course_year_range}/courses/{}" + course_search_tpl: str = ( + "http://www.mcgill.ca/study/{course_year_range}/courses/search?search_api_views_fulltext={}" + "&sort_by=field_subject_code" + "&page={}" + ) + gc_weather_url: str = "https://dd.weather.gc.ca/citypage_weather/xml/QC/s0000635_e.xml" + gc_weather_alert_url: str = "https://weather.gc.ca/warnings/report_e.html?qc67" + tepid_url: str = "https://tepid.science.mcgill.ca:8443/tepid/screensaver/queues/status" + + # Subscription configuration + recall_channel: str = "food" + recall_filter: str = "Quebec|National" + + # Currency configuration + currency: CurrencyModel = CurrencyModel() + + # Music configuration + music: MusicModel = MusicModel() + + # Images configuration + images: ImagesModel = ImagesModel() + + # Games configuration + games: GamesModel = GamesModel() + + # Assignable Roles + roles: RolesModel = RolesModel() + + class Config: # Pydantic config for our own Config class + env_file = ".env" + env_prefix = "CANARY_" + env_nested_delimiter = "__" diff --git a/Main.py b/canary/main.py similarity index 64% rename from Main.py rename to canary/main.py index fe932bb89..358cd7d8a 100755 --- a/Main.py +++ b/canary/main.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 # -# Copyright (C) idoneam (2016-2020) +# Copyright (C) idoneam (2016-2023) # # This file is part of Canary # @@ -16,84 +16,86 @@ # # You should have received a copy of the GNU General Public License # along with Canary. If not, see . - -# discord-py requirements +import aiosqlite import discord - -# Other utilities import os import sys -import subprocess + from datetime import datetime +from discord.ext.commands import Context from pytz import timezone -from bot import bot -import sqlite3 -from cogs.utils.checks import is_developer, is_moderator +from canary.bot import bot +from canary.cogs.utils.checks import is_developer, is_moderator startup = [ - "cogs.banner", - "cogs.currency", - "cogs.customreactions", - "cogs.games", - "cogs.helpers", - "cogs.images", - "cogs.info", - "cogs.memes", - "cogs.mod", - "cogs.music", - "cogs.quotes", - "cogs.reminder", - "cogs.roles", - "cogs.score", - "cogs.subscribers", # Do not remove this terminating comma. + f"canary.cogs.{c}" + for c in ( + "banner", + "currency", + "customreactions", + "games", + "helpers", + "images", + "info", + "memes", + "mod", + "music", + "quotes", + "reminder", + "roles", + "score", + "subscribers", # Do not remove this terminating comma. + ) ] @bot.event async def on_ready(): - if bot.config.dev_log_webhook_id and bot.config.dev_log_webhook_token: - webhook_string = " and to the log webhook" - else: - webhook_string = "" - sys.stdout.write(f"Bot is ready, program output will be written to a " f"log file{webhook_string}.\n") + webhook_string = ( + " and to the log webhook" if bot.config.dev_log_webhook_id and bot.config.dev_log_webhook_token else "" + ) + sys.stdout.write(f"Bot is ready, program output will be written to a log file{webhook_string}.\n") sys.stdout.flush() bot.dev_logger.info(f"Logged in as {bot.user.name} ({bot.user.id})") + await bot.health_check() @bot.command() @is_moderator() -async def load(ctx, extension_name: str): +async def load(ctx: Context, extension_name: str): """ Load a specific extension. Specify as cogs. """ + try: bot.load_extension(extension_name) except (AttributeError, ImportError) as e: - await ctx.send("```{}: {}\n```".format(type(e).__name__, str(e))) - + await ctx.send(f"```{type(e).__name__}: {str(e)}\n```") return - await ctx.send("{} loaded.".format(extension_name)) + + await ctx.send(f"{extension_name} loaded.") @bot.command() @is_moderator() -async def unload(ctx, extension_name: str): +async def unload(ctx: Context, extension_name: str): """ Unload a specific extension. Specify as cogs. """ + try: bot.unload_extension(extension_name) except Exception as e: - await ctx.send("```{}: {}\n```".format(type(e).__name__, str(e))) + await ctx.send(f"```{type(e).__name__}: {str(e)}\n```") return - await ctx.send("{} unloaded.".format(extension_name)) + await ctx.send(f"{extension_name} unloaded.") @bot.command() @is_developer() -async def restart(ctx): +async def restart(ctx: Context): """ Restart the bot """ @@ -105,7 +107,7 @@ async def restart(ctx): @bot.command() @is_moderator() -async def sleep(ctx): +async def sleep(ctx: Context): """ Shut down the bot """ @@ -116,7 +118,7 @@ async def sleep(ctx): @bot.command() @is_moderator() -async def backup(ctx): +async def backup(ctx: Context): """ Send the current database file to the owner """ @@ -130,11 +132,11 @@ async def backup(ctx): async def on_member_join(member): member_id = member.id name = str(member) - conn = sqlite3.connect(bot.config.db_path) - c = conn.cursor() - c.execute("INSERT OR REPLACE INTO Members VALUES (?,?)", (member_id, name)) - conn.commit() - conn.close() + + db: aiosqlite.Connection + async with bot.db() as db: + await db.execute("INSERT OR REPLACE INTO Members VALUES (?,?)", (member_id, name)) + await db.commit() @bot.listen() @@ -144,25 +146,27 @@ async def on_user_update(before, after): user_id = after.id new_name = str(after) - conn = sqlite3.connect(bot.config.db_path) - c = conn.cursor() - c.execute("INSERT OR REPLACE INTO Members VALUES (?,?)", (user_id, new_name)) - conn.commit() - conn.close() + + db: aiosqlite.Connection + async with bot.db() as db: + await db.execute("INSERT OR REPLACE INTO Members VALUES (?,?)", (user_id, new_name)) + await db.commit() def main(): + if os.name == "posix": + import uvloop + + uvloop.install() + for extension in startup: try: bot.load_extension(extension) except Exception as e: - bot.dev_logger.warning(f"Failed to load extension {extension}\n" f"{type(e).__name__}: {e}") + bot.dev_logger.warning(f"Failed to load extension {extension}\n{type(e).__name__}: {e}") + bot.run(bot.config.discord_key) if __name__ == "__main__": - if os.name == "posix": - import uvloop - - uvloop.install() main() diff --git a/cogs/customreactions.py b/cogs/customreactions.py deleted file mode 100644 index c5470a499..000000000 --- a/cogs/customreactions.py +++ /dev/null @@ -1,1235 +0,0 @@ -# Copyright (C) idoneam (2016-2021) -# -# This file is part of Canary -# -# Canary is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Canary is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Canary. If not, see . - -# discord-py requirements -import discord -from discord.ext import commands -import asyncio - -# Other utilities -import random -import sqlite3 -from .utils.paginator import Pages -import time -from .utils.p_strings import PStringEncodings - -EMOJI = { - "new": "🆕", - "mag": "🔍", - "pencil": "📝", - "stop_button": "⏹", - "ok": "🆗", - "white_check_mark": "✅", - "x": "❌", - "put_litter_in_its_place": "🚮", - "rewind": "⏪", - "arrow_backward": "◀", - "arrow_forward": "▶", - "fast_forward": "⏩", - "grey_question": "❔", - "zero": "0️⃣", - "one": "1️⃣", - "two": "2️⃣", - "three": "3️⃣", - "four": "4️⃣", - "five": "5️⃣", -} - -NUMBERS = (EMOJI["zero"], EMOJI["one"], EMOJI["two"], EMOJI["three"], EMOJI["four"], EMOJI["five"]) - -CUSTOM_REACTION_TIMEOUT = "Custom Reaction timed out. " "You may want to run the command again." -STOP_TEXT = "stop" -LOADING_EMBED = discord.Embed(title="Loading...") - - -class CustomReactions(commands.Cog): - # Written by @le-potate - def __init__(self, bot): - self.bot = bot - self.reaction_list = [] - self.proposal_list = [] - self.p_strings = None - self.rebuild_lists() - - def rebuild_lists(self): - self.rebuild_reaction_list() - self.rebuild_proposal_list() - - def rebuild_reaction_list(self): - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT * FROM CustomReactions WHERE Proposal = 0") - self.reaction_list = c.fetchall() - prompts = [row[1].lower() for row in self.reaction_list] - responses = [row[2] for row in self.reaction_list] - anywhere_values = [row[5] for row in self.reaction_list] - additional_info_list = [(row[4], row[6]) for row in self.reaction_list] - self.p_strings = PStringEncodings( - prompts, responses, anywhere_values, additional_info_list=additional_info_list - ) - conn.close() - - def rebuild_proposal_list(self): - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - c.execute("SELECT * FROM CustomReactions WHERE Proposal = 1") - self.proposal_list = c.fetchall() - conn.close() - - @commands.Cog.listener() - async def on_message(self, message): - if message.author == self.bot.user: - return - - response = self.p_strings.parser( - message.content.lower(), user=message.author.mention, channel=str(message.channel) - ) - if response: - # delete the prompt if DeletePrompt option is activated - if response.additional_info[0] == 1: - await message.delete() - - # send the response if DM option selected, - # send in the DM of the user who wrote the prompt - if response.additional_info[1] == 1: - await message.author.send(str(response)) - else: - await message.channel.send(str(response)) - - @commands.max_concurrency(1, per=commands.BucketType.user, wait=False) - @commands.command(aliases=["customreaction", "customreacts", "customreact"]) - async def customreactions(self, ctx): - current_options = [] - main_user = ctx.message.author - await ctx.message.delete() - - def get_number_of_proposals(): - return len(self.proposal_list) - - def get_reaction_check(moderators=False, reaction_user=None): - def reaction_check(reaction, user): - return all( - ( - reaction.emoji in current_options, - reaction.message.id == initial_message.id, - not moderators or discord.utils.get(user.roles, name=self.bot.config.moderator_role), - not reaction_user or user == reaction_user, - ) - ) - - return reaction_check - - def get_msg_check(msg_user=None): - def msg_check(msg): - if all((not msg_user or msg.author == msg_user, msg.channel == ctx.channel)): - if msg.attachments: - # in python 3.7, rewrite as - # asyncio.create_task(ctx.send([...])) - # (the get_event_loop() part isn't necessary) - loop = asyncio.get_event_loop() - loop.create_task(ctx.send("Attachments cannot be used, " "but you may use URLs")) - else: - return True - - return msg_check - - def get_number_check(msg_user=None, number_range=None): - def number_check(msg): - if msg.content.isdigit(): - return all( - ( - not msg_user or msg.author == msg_user, - not number_range or int(msg.content) in number_range, - msg.channel == ctx.channel, - ) - ) - - return number_check - - async def wait_for_reaction(message): - try: - reaction, user = await self.bot.wait_for( - "reaction_add", check=get_reaction_check(reaction_user=main_user), timeout=60 - ) - except asyncio.TimeoutError: - await message.clear_reactions() - await message.edit(embed=discord.Embed(title=CUSTOM_REACTION_TIMEOUT), delete_after=60) - return - return reaction, user - - async def wait_for_message(message): - try: - msg = await self.bot.wait_for("message", check=get_msg_check(msg_user=main_user), timeout=60) - except asyncio.TimeoutError: - await message.clear_reactions() - await message.edit(embed=discord.Embed(title=CUSTOM_REACTION_TIMEOUT), delete_after=60) - return - content = msg.content - await msg.delete() - return content - - async def add_multiple_reactions(message, reactions): - for reaction in reactions: - await message.add_reaction(reaction) - - async def add_yes_or_no_reactions(message): - await add_multiple_reactions(message, (EMOJI["zero"], EMOJI["one"], EMOJI["stop_button"])) - - async def add_control_reactions(message): - await add_multiple_reactions( - message, - ( - EMOJI["rewind"], - EMOJI["arrow_backward"], - EMOJI["arrow_forward"], - EMOJI["fast_forward"], - EMOJI["stop_button"], - EMOJI["ok"], - ), - ) - - async def create_assistant(message, is_moderator): - if is_moderator: - description = ( - f"{EMOJI['new']} Add a new custom reaction\n" - f"{EMOJI['mag']} See the list of current reactions " - f"and modify them\n" - f"{EMOJI['pencil']} See the list of proposed reactions " - f"({get_number_of_proposals()}) " - f"and approve or reject them\n" - f"{EMOJI['grey_question']} List of placeholders" - ) - else: - description = ( - f"{EMOJI['new']} Propose a new custom reaction\n" - f"{EMOJI['mag']} See the list of current reactions\n" - f"{EMOJI['pencil']} See the list of proposed reactions " - f"({get_number_of_proposals()})\n" - f"{EMOJI['grey_question']} List of placeholders" - ) - current_options.extend( - (EMOJI["new"], EMOJI["mag"], EMOJI["pencil"], EMOJI["grey_question"], EMOJI["stop_button"]) - ) - await add_multiple_reactions( - message, (EMOJI["new"], EMOJI["mag"], EMOJI["pencil"], EMOJI["grey_question"], EMOJI["stop_button"]) - ) - await message.edit( - embed=discord.Embed(title="Custom Reactions", description=description).set_footer( - text=f"{main_user}: Click on an emoji to choose an " - f"option (If a list is chosen, all users " - f"will be able to interact with it)", - icon_url=main_user.avatar_url, - ) - ) - try: - reaction, user = await wait_for_reaction(message) - except TypeError: - return - current_options.clear() - await message.clear_reactions() - # Add/Propose a new custom reaction - if reaction.emoji == EMOJI["new"]: - await add_custom_react(message, is_moderator) - return - # List custom reactions - if reaction.emoji == EMOJI["mag"]: - await list_custom_reacts(message, proposals=False) - return - # List proposals - if reaction.emoji == EMOJI["pencil"]: - await list_custom_reacts(message, proposals=True) - return - # List placeholders - if reaction.emoji == EMOJI["grey_question"]: - await list_placeholders(message) - return - # Stop - if reaction.emoji == EMOJI["stop_button"]: - await leave(message) - return True - - async def add_custom_react(message, is_moderator): - if is_moderator: - title = "Add a custom reaction" - footer = f"{main_user} is currently adding a custom reaction. \n" f"Write '{STOP_TEXT}' to cancel." - else: - title = "Propose a custom reaction" - footer = ( - f"{main_user} is currently proposing a custom " f"reaction. \n" f"Write '{STOP_TEXT}' to cancel." - ) - description = "Write the prompt the bot will react to" - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=main_user.avatar_url - ) - ) - prompt_message = await wait_for_message(message) - if prompt_message is None: - return - if prompt_message.lower() == STOP_TEXT: - await leave(message) - return True - description = f"Prompt: {prompt_message}\nWrite the response " f"the bot will send" - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=main_user.avatar_url - ) - ) - response = await wait_for_message(message) - if response is None: - return - if response.lower() == STOP_TEXT: - await leave(message) - return True - await message.edit(embed=LOADING_EMBED) - description = ( - f"Prompt: {prompt_message}\nResponse: {response}\n" - f"React with the options " - f"you want and click {EMOJI['ok']} " - f"when you are ready\n" - f"{EMOJI['one']} Delete the message " - f"that calls the reaction\n" - f"{EMOJI['two']} Activate the custom " - f"reaction if the prompt is " - f"anywhere in a message \n" - f"{EMOJI['three']} React in the DMs of " - f"the user who calls the " - f"reaction instead of the channel\n" - ) - if is_moderator: - footer = f"{main_user} is currently adding a custom reaction." - else: - footer = f"{main_user} is currently " f"proposing a custom reaction." - current_options.extend((EMOJI["ok"], EMOJI["stop_button"])) - await add_multiple_reactions(message, (*NUMBERS[1:4], EMOJI["ok"], EMOJI["stop_button"])) - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=main_user.avatar_url - ) - ) - try: - reaction, user = await wait_for_reaction(message) - except TypeError: - return - - # If the user clicked OK, check if delete/anywhere/dm are checked - if reaction.emoji == EMOJI["ok"]: - delete = False - anywhere = False - dm = False - cache_msg = await message.channel.fetch_message(message.id) - for reaction in cache_msg.reactions: - users_who_reacted = await reaction.users().flatten() - if main_user in users_who_reacted: - delete = delete or reaction.emoji == EMOJI["one"] - anywhere = anywhere or reaction.emoji == EMOJI["two"] - dm = dm or reaction.emoji == EMOJI["three"] - - current_options.clear() - await message.clear_reactions() - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - t = (prompt_message, response, main_user.id, delete, anywhere, dm, not is_moderator) - c.execute( - "INSERT INTO CustomReactions(Prompt, Response, UserID, " - "DeletePrompt, Anywhere, DM, Proposal) " - "VALUES(?,?,?,?,?,?,?)", - t, - ) - conn.commit() - conn.close() - self.rebuild_lists() - - if is_moderator: - title = "Custom reaction successfully added!" - else: - title = f"Custom reaction proposal successfully submitted!" - description = f"-Prompt: {prompt_message}\n" f"-Response: {response}" - if delete: - description = f"{description}\n-Will delete the " f"message that calls the reaction" - if anywhere: - description = ( - f"{description}\n" - f"-Will activate the custom reaction " - "if the prompt is anywhere in a message" - ) - if dm: - description = ( - f"{description}\n" - f"-Will react in the DMs of the user " - f"who calls the reaction instead of the " - f"channel" - ) - - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=f"Added by {main_user}.", icon_url=main_user.avatar_url - ) - ) - - return - - # Stop - if reaction.emoji == EMOJI["stop_button"]: - await leave(message) - return True - - async def list_custom_reacts(message, proposals): - current_list = self.proposal_list if proposals else self.reaction_list - - if not current_list: - if proposals: - title = "There are currently no custom reaction " "proposals in this server" - else: - title = "There are currently no custom reactions in " "this server" - await message.edit(embed=discord.Embed(title=title), delete_after=60) - return - - reaction_dict = { - "names": [f"[{i + 1}]" for i in range(len(current_list))], - "values": [ - f"Prompt: " - f"{reaction[1][:min(len(reaction[1]), 287)]}" - f'{"..." if len(reaction[1]) > 287 else ""}' - f"\nResponse: " - f"{reaction[2][:min(len(reaction[2]), 287)]}" - f'{"..." if len(reaction[2]) > 287 else ""}' - for reaction in current_list - ], - } - - await message.edit(embed=LOADING_EMBED) - - await add_control_reactions(message) - - if proposals: - title = ( - f"Current custom reaction proposals\n" - f"Click on {EMOJI['ok']} " - f"to approve, reject, edit, or see more " - f"information on one of them" - ) - else: - title = ( - f"Current custom reactions\nClick on {EMOJI['ok']} " - f"to edit or see more information on one of them" - ) - - p = Pages( - ctx, - msg=message, - item_list=reaction_dict, - title=title, - display_option=(2, 10), - editable_content_emoji=EMOJI["ok"], - return_user_on_edit=True, - ) - - user_modifying = await p.paginate() - while p.edit_mode: - await message.clear_reactions() - if proposals: - title = ( - f"Current custom reaction proposals\n" - f"{user_modifying}: Write the number of the " - f"custom reaction " - f"proposal you want to approve, reject, edit, or " - f"see more information on" - ) - else: - title = ( - f"Current custom reactions\n" - f"{user_modifying}: Write the number of the " - f"custom reaction " - f"you want to edit or see more " - f"information on" - ) - message.embeds[0].title = title - await message.edit(embed=message.embeds[0]) - number = 0 - try: - msg = await self.bot.wait_for( - "message", - check=get_number_check(msg_user=user_modifying, number_range=range(1, len(current_list) + 1)), - timeout=60, - ) - number = int(msg.content) - await msg.delete() - except asyncio.TimeoutError: - pass - - if number == 0: - if proposals: - title = ( - f"Current custom reaction proposals\n" - f"Click on {EMOJI['ok']} " - f"to approve, reject, edit, or " - f"see more information on one of them " - f"(Previous attempt received invalid input " - f"or timed out)" - ) - else: - title = ( - f"Current custom reactions\n" - f"Click on {EMOJI['ok']} " - f"to edit or see more information on one of " - f"them (Previous attempt received invalid " - f"input or timed out)" - ) - p = Pages( - ctx, - msg=message, - item_list=reaction_dict, - title=title, - display_option=(2, 10), - editable_content_emoji=EMOJI["ok"], - return_user_on_edit=True, - ) - else: - left = await information_on_react(message, current_list, number, proposals) - if left: - return True - if proposals: - title = ( - f"Current custom reaction proposals\n" - f"Click on {EMOJI['ok']} " - f"to approve, reject, edit, or " - f"see more information on one of them" - ) - else: - title = ( - f"Current custom reactions\n" - f"Click on {EMOJI['ok']} " - f"to edit or see more information " - f"on one of them" - ) - - # update dictionary since a custom reaction might have been - # modified - current_list = self.proposal_list if proposals else self.reaction_list - - if not current_list: - if proposals: - title = "There are currently no custom " "reaction proposals in this server" - else: - title = "There are currently no custom " "reactions in this server" - await message.edit(embed=discord.Embed(title=title), delete_after=60) - return - - reaction_dict = { - "names": [f"[{i + 1}]" for i in range(len(current_list))], - "values": [ - f"Prompt: " - f"{reaction[1][:min(len(reaction[1]), 287)]}" - f'{"..." if len(reaction[1]) > 287 else ""}' - f"\nResponse: " - f"{reaction[2][:min(len(reaction[2]), 287)]}" - f'{"..." if len(reaction[2]) > 287 else ""}' - for reaction in current_list - ], - } - - p = Pages( - ctx, - msg=message, - item_list=reaction_dict, - title=title, - display_option=(2, 10), - editable_content_emoji=EMOJI["ok"], - return_user_on_edit=True, - ) - await message.edit(embed=LOADING_EMBED) - - await add_control_reactions(message) - - user_modifying = await p.paginate() - - async def information_on_react(message, current_list, number, proposals): - await message.edit(embed=LOADING_EMBED) - - custom_react = current_list[number - 1] - prompt = custom_react[1] - response = custom_react[2] - user_who_added = self.bot.get_user(custom_react[3]) - delete = custom_react[4] - anywhere = custom_react[5] - dm = custom_react[6] - if delete == 1: - delete_str = "Deletes the message that calls the reaction" - else: - delete_str = "Does not delete the message that " "calls the reaction" - if anywhere == 1: - anywhere_str = "Activates the custom reaction if " "the prompt is anywhere in a message" - else: - anywhere_str = "Only activates the custom reaction " "if the prompt is the full message" - if dm == 1: - dm_str = "Reacts in the DMs of the user who calls " "the reaction instead of the channel" - else: - dm_str = "Reacts directly into the channel" - - if proposals: - description = ( - f"{EMOJI['one']} Prompt: {prompt}" - f"\n{EMOJI['two']} Response: {response}" - f"\n{EMOJI['three']} {delete_str}" - f"\n{EMOJI['four']} {anywhere_str}" - f"\n{EMOJI['five']} {dm_str}" - f"\n{EMOJI['white_check_mark']} " - f"Approve this proposal\n" - f"{EMOJI['x']} Reject this proposal\n" - f"Added by {user_who_added}" - ) - title = ( - f"More information on a custom reaction proposal.\n" - f"{self.bot.config.moderator_role}s " - f"may click on emojis to modify those values or " - f"approve/refuse this proposal\n" - f"(Will return to the list of current reaction " - f"proposals in 40 seconds otherwise)" - ) - else: - description = ( - f"{EMOJI['one']} Prompt: {prompt}\n" - f"{EMOJI['two']} Response: {response}" - f"\n{EMOJI['three']} {delete_str}" - f"\n{EMOJI['four']} {anywhere_str}" - f"\n{EMOJI['five']} {dm_str}" - f"\n{EMOJI['put_litter_in_its_place']} " - f"Delete this custom reaction\n" - f"Added by {user_who_added}" - ) - title = ( - f"More information on a custom reaction.\n" - f"{self.bot.config.moderator_role}s may click " - f"on emojis to modify those values " - f"or select an option\n(Will return to the list of " - f"current reactions in 40 seconds otherwise)" - ) - - current_options.clear() - await message.clear_reactions() - if proposals: - current_options.extend((*NUMBERS[1:6], EMOJI["white_check_mark"], EMOJI["x"], EMOJI["stop_button"])) - else: - current_options.extend((*NUMBERS[1:6], EMOJI["put_litter_in_its_place"], EMOJI["stop_button"])) - if proposals: - await add_multiple_reactions( - message, (*NUMBERS[1:6], EMOJI["white_check_mark"], EMOJI["x"], EMOJI["stop_button"]) - ) - else: - await add_multiple_reactions( - message, (*NUMBERS[1:6], EMOJI["put_litter_in_its_place"], EMOJI["stop_button"]) - ) - await message.edit(embed=discord.Embed(title=title, description=description)) - - try: - reaction, user = await self.bot.wait_for( - "reaction_add", check=get_reaction_check(moderators=True), timeout=40 - ) - left = await edit_custom_react(message, reaction, user, custom_react, proposals) - if left: - return True - except asyncio.TimeoutError: - pass - current_options.clear() - await message.clear_reactions() - - async def edit_custom_react(message, reaction, user, custom_react, proposals): - current_options.clear() - await message.clear_reactions() - custom_react_id = custom_react[0] - delete = custom_react[4] - anywhere = custom_react[5] - dm = custom_react[6] - conn = sqlite3.connect(self.bot.config.db_path) - c = conn.cursor() - - # Edit the prompt - if reaction.emoji == EMOJI["one"]: - if proposals: - title = "Modify a custom reaction proposal" - footer = ( - f"{user} is currently modifying " - f"a custom reaction proposal. \n" - f"Write '{STOP_TEXT}' to cancel." - ) - else: - title = "Modify a custom reaction" - footer = ( - f"{user} is currently " f"modifying a custom reaction. " f"\nWrite '{STOP_TEXT}' to cancel." - ) - description = "Please enter the new prompt" - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=user.avatar_url - ) - ) - try: - msg = await self.bot.wait_for("message", check=get_msg_check(msg_user=user), timeout=60) - - except asyncio.TimeoutError: - if proposals: - title = ( - "The modification of the custom reaction " - "proposal timed out. " - "Returning to list of reaction proposals..." - ) - else: - title = ( - "The modification of the custom reaction " - "timed out. " - "Returning to list of current reactions..." - ) - await message.edit(embed=discord.Embed(title=title)) - conn.close() - await asyncio.sleep(5) - return - - prompt = msg.content - await msg.delete() - - if prompt.lower() == STOP_TEXT: - await leave(message) - return True - - t = (prompt, custom_react_id) - c.execute("UPDATE CustomReactions SET Prompt = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = "Prompt successfully modified! " "Returning to list of reaction proposals..." - else: - title = "Prompt successfully modified! " "Returning to list of current reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer(text=f"Modified by {user}.", icon_url=user.avatar_url) - ) - conn.close() - await asyncio.sleep(5) - - # Edit the response - if reaction.emoji == EMOJI["two"]: - if proposals: - title = "Modify a custom reaction proposal" - footer = ( - f"{user} is currently modifying a " - f"custom reaction proposal. \n" - f"Write '{STOP_TEXT}' to cancel." - ) - else: - title = "Modify a custom reaction" - footer = ( - f"{user} is currently modifying a " f"custom reaction. " f"\nWrite '{STOP_TEXT}' to cancel." - ) - description = "Please enter the new response" - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=user.avatar_url - ) - ) - - try: - msg = await self.bot.wait_for("message", check=get_msg_check(msg_user=user), timeout=60) - - except asyncio.TimeoutError: - if proposals: - title = ( - "The modification of the custom reaction " - "proposal timed out. " - "Returning to list of reaction proposals..." - ) - else: - title = ( - "The modification of the custom reaction " - "timed out. " - "Returning to list of current reactions..." - ) - await message.edit(embed=discord.Embed(title=title)) - conn.close() - await asyncio.sleep(5) - return - - response = msg.content - await msg.delete() - - if response.lower() == STOP_TEXT: - await leave(message) - return True - - t = (response, custom_react_id) - c.execute("UPDATE CustomReactions SET Response = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = "Response successfully modified! " "Returning to list of reaction proposals..." - else: - title = "Response successfully modified! " "Returning to list of current reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer(text=f"Modified by {user}.", icon_url=user.avatar_url) - ) - conn.close() - await asyncio.sleep(5) - - # Edit the "delete" option - if reaction.emoji == EMOJI["three"]: - await message.edit(embed=LOADING_EMBED) - if proposals: - title = "Modify a custom reaction proposal. " "React with the option you want" - footer = f"{user} is currently modifying a " f"custom reaction proposal. \n" - else: - title = "Modify a custom reaction. React with the " "option you want" - footer = f"{user} is currently modifying a " f"custom reaction. \n" - description = ( - f"Should the message that calls the " - f"reaction be deleted?\n" - f"{EMOJI['zero']} No\n" - f"{EMOJI['one']} Yes" - ) - current_options.clear() - await message.clear_reactions() - current_options.extend((*NUMBERS[0:2], EMOJI["stop_button"])) - await add_yes_or_no_reactions(message) - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=user.avatar_url - ) - ) - - try: - reaction, reaction_user = await self.bot.wait_for( - "reaction_add", check=get_reaction_check(reaction_user=user), timeout=60 - ) - - except asyncio.TimeoutError: - if proposals: - title = ( - "The modification of the custom reaction " - "proposal timed out. " - "Returning to list of reaction proposals..." - ) - else: - title = ( - "The modification of the custom reaction " - "timed out. " - "Returning to list of current reactions..." - ) - await message.edit(embed=discord.Embed(title=title)) - conn.close() - await asyncio.sleep(5) - current_options.clear() - await message.clear_reactions() - return - - current_options.clear() - await message.clear_reactions() - # Deactivate the "delete" option - if reaction.emoji == EMOJI["zero"]: - if delete == 0: - if proposals: - title = "Successfully kept current option! " "Returning to list of reaction " "proposals..." - else: - title = "Successfully kept current option! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - else: - t = (0, custom_react_id) - c.execute("UPDATE CustomReactions SET DeletePrompt = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = ( - "Option successfully modified! " "Returning to list of current " "reaction proposals..." - ) - else: - title = "Option successfully modified! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - - # Activate the "delete" option - elif reaction.emoji == EMOJI["one"]: - if delete == 1: - if proposals: - title = "Successfully kept current option! " "Returning to list of " "reaction proposals..." - else: - title = "Successfully kept current option! " "Returning to list of " "current reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - else: - t = (1, custom_react_id) - c.execute("UPDATE CustomReactions SET DeletePrompt = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = ( - "Option successfully modified! " "Returning to list of current " "reaction proposals..." - ) - else: - title = "Option successfully modified! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - # Stop - elif reaction.emoji == EMOJI["stop_button"]: - await leave(message) - return True - - # Edit the "anywhere" option - if reaction.emoji == EMOJI["four"]: - await message.edit(embed=LOADING_EMBED) - if proposals: - title = "Modify a custom reaction proposal. " "React with the option you want" - footer = f"{user} is currently modifying a custom " f"reaction proposal. \n" - else: - title = "Modify a custom reaction. " "React with the option you want" - footer = f"{user} is currently modifying a custom " f"reaction. \n" - description = ( - f"Should the custom reaction be activated " - f"if the prompt is anywhere in a message?\n" - f"{EMOJI['zero']} No\n" - f"{EMOJI['one']} Yes" - ) - current_options.clear() - await message.clear_reactions() - current_options.extend((*NUMBERS[0:2], EMOJI["stop_button"])) - await add_yes_or_no_reactions(message) - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=user.avatar_url - ) - ) - try: - reaction, reaction_user = await self.bot.wait_for( - "reaction_add", check=get_reaction_check(reaction_user=user), timeout=60 - ) - - except asyncio.TimeoutError: - if proposals: - title = ( - "The modification of the custom reaction " - "proposal timed out. " - "Returning to list of reaction proposals..." - ) - else: - title = ( - "The modification of the custom reaction " - "timed out. " - "Returning to list of current reactions..." - ) - await message.edit(embed=discord.Embed(title=title)) - conn.close() - await asyncio.sleep(5) - current_options.clear() - await message.clear_reactions() - return - - current_options.clear() - await message.clear_reactions() - # Deactivate "anywhere" option - if reaction.emoji == EMOJI["zero"]: - if anywhere == 0: - if proposals: - title = "Successfully kept current option! " "Returning to list of " "reaction proposals..." - else: - title = "Successfully kept current option! " "Returning to list of " "current reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - else: - t = (0, custom_react_id) - c.execute("UPDATE CustomReactions SET Anywhere = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = ( - "Option successfully modified! " "Returning to list of current " "reaction proposals..." - ) - else: - title = "Option successfully modified! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - - # Activate "anywhere" option - elif reaction.emoji == EMOJI["one"]: - if anywhere == 1: - if proposals: - title = "Successfully kept current option! " "Returning to list of " "reaction proposals..." - else: - title = "Successfully kept current option! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - else: - t = (1, custom_react_id) - c.execute("UPDATE CustomReactions SET Anywhere = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = ( - "Option successfully modified! " "Returning to list of current " "reaction proposals..." - ) - else: - title = "Option successfully modified! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - # Stop - elif reaction.emoji == EMOJI["stop_button"]: - await leave(message) - return True - - # Edit "dm" option - if reaction.emoji == EMOJI["five"]: - await message.edit(embed=LOADING_EMBED) - if proposals: - title = "Modify a custom reaction proposal. " "React with the option you want" - footer = f"{user} is currently modifying a custom " f"reaction " f"proposal. \n" - else: - title = "Modify a custom reaction. React with the " "option you want" - footer = f"{user} is currently modifying a " f"custom reaction. \n" - description = ( - f"Should the reaction be sent in the DMs of " - f"the user who called the reaction " - f"instead of the channel?\n" - f"{EMOJI['zero']} No\n" - f"{EMOJI['one']} Yes" - ) - current_options.clear() - await message.clear_reactions() - current_options.extend((*NUMBERS[0:2], EMOJI["stop_button"])) - await add_yes_or_no_reactions(message) - await message.edit( - embed=discord.Embed(title=title, description=description).set_footer( - text=footer, icon_url=user.avatar_url - ) - ) - try: - reaction, reaction_user = await self.bot.wait_for( - "reaction_add", check=get_reaction_check(reaction_user=user), timeout=60 - ) - - except asyncio.TimeoutError: - if proposals: - title = ( - "The modification of the custom reaction " - "proposal timed out. " - "Returning to list of reaction proposals..." - ) - else: - title = ( - "The modification of the custom reaction " - "timed out. " - "Returning to list of current reactions..." - ) - await message.edit(embed=discord.Embed(title=title)) - conn.close() - await asyncio.sleep(5) - current_options.clear() - await message.clear_reactions() - return - - current_options.clear() - await message.clear_reactions() - # Deactivate "dm" option - if reaction.emoji == EMOJI["zero"]: - if dm == 0: - if proposals: - title = "Successfully kept current option! " "Returning to list of " "reaction proposals..." - else: - title = "Successfully kept current option! " "Returning to list of " "current reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - else: - t = (0, custom_react_id) - c.execute("UPDATE CustomReactions SET DM = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = ( - "Option successfully modified! " "Returning to list of current " "reaction proposals..." - ) - else: - title = "Option successfully modified! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - # Activate "dm" option - elif reaction.emoji == EMOJI["one"]: - if dm == 1: - if proposals: - title = "Successfully kept current option! " "Returning to list of " "reaction proposals..." - else: - title = "Successfully kept current option! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - else: - t = (1, custom_react_id) - c.execute("UPDATE CustomReactions SET DM = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - if proposals: - title = ( - "Option successfully modified! " "Returning to list of current " "reaction proposals..." - ) - else: - title = "Option successfully modified! " "Returning to list of current " "reactions..." - await message.edit( - embed=discord.Embed(title=title).set_footer( - text=f"Modified by {user}.", icon_url=user.avatar_url - ) - ) - conn.close() - await asyncio.sleep(5) - # Stop - elif reaction.emoji == EMOJI["stop_button"]: - await leave(message) - return True - - # Approve a custom reaction proposal - if reaction.emoji == EMOJI["white_check_mark"]: - t = (0, custom_react_id) - c.execute("UPDATE CustomReactions SET Proposal = ? " "WHERE CustomReactionID = ?", t) - conn.commit() - self.rebuild_lists() - title = ( - "Custom reaction proposal successfully approved! " - "Returning to list of current reaction proposals..." - ) - footer = f"Approved by {user}." - await message.edit(embed=discord.Embed(title=title).set_footer(text=footer, icon_url=user.avatar_url)) - conn.close() - await asyncio.sleep(5) - - # Delete a custom reaction or proposal - if reaction.emoji == EMOJI["put_litter_in_its_place"] or reaction.emoji == EMOJI["x"]: - t = (custom_react_id,) - c.execute("DELETE FROM CustomReactions WHERE CustomReactionID = ?", t) - conn.commit() - if proposals: - title = ( - "Custom reaction proposal successfully " - "rejected! Returning to list of current " - "reaction proposals..." - ) - footer = f"Rejected by {user}." - else: - title = "Custom reaction successfully deleted! " "Returning to list of current reactions..." - footer = f"Deleted by {user}." - await message.edit(embed=discord.Embed(title=title).set_footer(text=footer, icon_url=user.avatar_url)) - conn.close() - self.rebuild_lists() - await asyncio.sleep(5) - - # Stop - if reaction.emoji == EMOJI["stop_button"]: - await leave(message) - return True - - async def list_placeholders(message): - title = "The following placeholders can be used in " "prompts and responses:" - description = ( - "-%user%: the user who called " - "the prompt (can only be used in a response)\n" - "-%channel%: the name of " - "the channel where the prompt was called " - "(can only be used in a response) \n" - "-%1%, %2%, etc. up to %9%: Groups. When a " - "prompt uses this, anything will match. For " - 'example, the prompt "i %1% apples" will work ' - 'for any message that starts with "i" and ends ' - 'with "apples", such as "i really like ' - 'apples". Then, the words that match to this ' - "group can be used in the response. For example, " - "keeping the same prompt and using the response " - '"i %1% pears" will send ' - '"i really like pears"\n' - "-%[]%: a comma-separated choice list. There are " - "two uses for this. The first is that when it is " - "used in a prompt, the prompt will accept either " - "of the choices. For example, the prompt " - '"%[hello, hi, hey]% world" will work if someone ' - 'writes "hello world", "hi world" or ' - '"hey world". The second use is that when it is ' - "used in a response, a random choice will be " - "chosen from the list. For example, the response " - '"i %[like, hate]% you" will either send "i ' - 'like you" or "i hate you". All placeholders ' - "can be used in choice lists (including choice " - "lists themselves). If a choice contains commas, " - 'it can be surrounded by "" to not be split into ' - "different choices" - ) - await message.edit(embed=discord.Embed(title=title, description=description)) - - async def leave(message): - await message.delete() - - initial_message = await ctx.send(embed=LOADING_EMBED) - is_mod = discord.utils.get(main_user.roles, name=self.bot.config.moderator_role) is not None - await create_assistant(initial_message, is_mod) - - -def setup(bot): - bot.add_cog(CustomReactions(bot)) diff --git a/cogs/utils/__init__.py b/cogs/utils/__init__.py deleted file mode 100644 index b30f0b2c9..000000000 --- a/cogs/utils/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) idoneam (2016-2019) -# -# This file is part of Canary -# -# Canary is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Canary is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Canary. If not, see . diff --git a/config/config.ini b/config/config.ini deleted file mode 100644 index 4923b4217..000000000 --- a/config/config.ini +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (C) idoneam (2016-2022) -# -# This file is part of Canary -# -# Canary is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Canary is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Canary. If not, see . - -[Discord] -Key = KEY - -[Server] -ServerID = 236668784948019202 -CommandPrefix = ? -BotName = Marty - -[Emoji] -UpvoteEmoji = upmartlet -DownvoteEmoji = downmartlet -BannerVoteEmoji = redchiken - -[Roles] -ModeratorRole = Discord Moderator -DeveloperRole = idoneam -McGillianRole = McGillian -HonoraryMcGillianRole = Honorary McGillian -BannerRemindersRole = Banner Submissions -BannerWinnerRole = Banner of the Week Winner -TrashTierBannerRole = Trash Tier Banner Submissions -NoFoodSpottingRole = Trash Tier Foodspotting -MutedRole = Muted -CrabboRole = Secret Crabbo - -[Channels] -ReceptionChannel = martys_dm -BannerOfTheWeekChannel = banner_of_the_week -BannerSubmissionsChannel = banner_submissions -BannerConvertedChannel = converted_banner_submissions -FoodSpottingChannel = foodspotting -MetroStatusChannel = stm_alerts -BotsChannel = bots -VerificationChannel = verification_log -AppealsLogChannel = appeals_log -AppealsCategory = appeals - -[Meta] -Repository = https://github.com/idoneam/Canary.git - -[Logging] -LogLevel = info -LogFile = ./canary.log -DevLogWebhookID = -DevLogWebhookToken = -ModLogWebhookID = -ModLogWebhookToken = - -[DB] -Schema = ./Martlet.schema -Path = ./data/runtime/Martlet.db - -[Greetings] -Welcome = -Goodbye = - -[Helpers] -CourseTemplate = http://www.mcgill.ca/study/2020-2021/courses/{} -CourseSearchTemplate = http://www.mcgill.ca/study/2020-2021/courses/search?search_api_views_fulltext={}&sort_by=field_subject_code&page={} -GCWeatherURL = http://weather.gc.ca/city/pages/qc-147_metric_e.html -GCWeatherAlertURL = https://weather.gc.ca/warnings/report_e.html?qc67 -WttrINTemplate = http://wttr.in/Montreal_2mpq_lang=en.png?_m -TepidURL = https://tepid.science.mcgill.ca:8443/tepid/screensaver/queues/status - -[Subscribers] -FoodRecallChannel = food -FoodRecallLocationFilter = Quebec|National - -[Currency] -Name = cheeps -Symbol = ʧ -Precision = 2 -Initial = 1000 -SalaryBase = 200 -Inflation = 0.005 - -[IncomeTax] -Brackets = 100, 300, 1000, Infinity -Amounts = 0, 0.15, 0.30, 0.50 - -[AssetTax] -Brackets = 5000, 10000, Infinity -Amounts = 0, 0.20, 0.50 - -[OtherTax] -TransactionTax = 0.15 - -[Betting] -RollCases = 66, 90, 99, 100 -RollReturns = 0, 2, 4, 10 - -[Music] -BanRole = tone deaf -StartVol = 100 - -[Images] -MaxImageSize = 8000000 -ImageHistoryLimit = 50 -MaxRadius = 500 -MaxIterations = 20 - -[Games] -HangmanNormalWin = 10 -HangmanCoolWin = 20 -HangmanTimeOut = 600 - -[AssignableRoles] -Pronouns = She/Her, He/Him, They/Them -Fields = Accounting, Agriculture, Anatomy and Cell Biology, Anthropology, Architecture, Biochemistry, Bioengineering, Biology, Bioresource Engineering, Chemical Engineering, Chemistry, Civil Engineering, Classics, cogito, Commerce, Computer Engineering, Computer Science, Computer Science/Biology, Cultural Studies, Desautels, Economics, Electrical Engineering, English, Experimental Medicine, Finance, Geography, History, Human Genetics, Indigenous Studies, International Development Studies, Jewish Studies, linguini, mac kid, Materials Engineering, Math, MBA, Mechanical Engineering, Medicine, Microbiology and Immunology, Neuroscience, Nursing, Pharmacology, Philosophy, Physical Therapy, Physics, Physiology, Political Science, Psychiatry, Psychology, Public Health, Social Work, Sociology, Software Engineering, Statistics, Theology, Urban Systems -Faculties = Science, Engineering, Management, art you glad you're not in arts, ArtSci, Agriculture and Environment, Continuing Studies, Law, Education, Dentistry, Music -Years = U0, U1, U2, U3, U4, grad student, workhere, wenthere -Generics = weeb, weeb stomper, crosswords, stm_alertee, Stardew, R6, CS:GO, Minecraft, Among Us, Pokemon Go, Secret Crabbo, Warzone, Monster Hunter, undersad diff --git a/config/parser.py b/config/parser.py deleted file mode 100644 index 58611bd1f..000000000 --- a/config/parser.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (C) idoneam (2016-2022) -# -# This file is part of Canary -# -# Canary is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Canary is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Canary. If not, see . - -import codecs -import configparser - -import logging -import decimal # Currency - -LOG_LEVELS = { - "critical": logging.CRITICAL, - "error": logging.ERROR, - "warning": logging.WARNING, - "info": logging.INFO, - "debug": logging.DEBUG, - "notset": logging.NOTSET, -} - - -class Parser: - def __init__(self): - self.configfile = "./config/config.ini" - - config = configparser.ConfigParser() - config.read_file(codecs.open(self.configfile, "r", "utf-8-sig")) - - # Discord token - self.discord_key = config["Discord"]["Key"] - - # Server configs - self.server_id = int(config["Server"]["ServerID"]) - self.command_prefix = [s for s in config["Server"]["CommandPrefix"].strip().split(",")] - self.bot_name = config["Server"]["BotName"] - - # Emoji - self.upvote_emoji = config["Emoji"]["UpvoteEmoji"] - self.downvote_emoji = config["Emoji"]["DownvoteEmoji"] - self.banner_vote_emoji = config["Emoji"]["BannerVoteEmoji"] - - # Roles - self.moderator_role = config["Roles"]["ModeratorRole"] - self.developer_role = config["Roles"]["DeveloperRole"] - self.mcgillian_role = config["Roles"]["McGillianRole"] - self.honorary_mcgillian_role = config["Roles"]["HonoraryMcGillianRole"] - self.banner_reminders_role = config["Roles"]["BannerRemindersRole"] - self.banner_winner_role = config["Roles"]["BannerWinnerRole"] - self.trash_tier_banner_role = config["Roles"]["TrashTierBannerRole"] - self.no_food_spotting_role = config["Roles"]["NoFoodSpottingRole"] - self.muted_role = config["Roles"]["MutedRole"] - self.crabbo_role = config["Roles"]["CrabboRole"] - - # Channels - self.reception_channel = config["Channels"]["ReceptionChannel"] - self.banner_of_the_week_channel = config["Channels"]["BannerOfTheWeekChannel"] - self.banner_submissions_channel = config["Channels"]["BannerSubmissionsChannel"] - self.banner_converted_channel = config["Channels"]["BannerConvertedChannel"] - self.food_spotting_channel = config["Channels"]["FoodSpottingChannel"] - self.metro_status_channel = config["Channels"]["MetroStatusChannel"] - self.bots_channel = config["Channels"]["BotsChannel"] - self.verification_channel = config["Channels"]["VerificationChannel"] - self.appeals_log_channel = config["Channels"]["AppealsLogChannel"] - self.appeals_category = config["Channels"]["AppealsCategory"] - - # Meta - self.repository = config["Meta"]["Repository"] - - # Logging - self.log_file = config["Logging"]["LogFile"] - loglevel = config["Logging"]["LogLevel"].lower() - self.log_level = LOG_LEVELS.get(loglevel, logging.WARNING) - if config["Logging"]["DevLogWebhookID"] and config["Logging"]["DevLogWebhookToken"]: - self.dev_log_webhook_id = int(config["Logging"]["DevLogWebhookID"]) - self.dev_log_webhook_token = config["Logging"]["DevLogWebhookToken"] - else: - self.dev_log_webhook_id = None - self.dev_log_webhook_token = None - if config["Logging"]["ModLogWebhookID"] and config["Logging"]["ModLogWebhookToken"]: - self.mod_log_webhook_id = int(config["Logging"]["ModLogWebhookID"]) - self.mod_log_webhook_token = config["Logging"]["ModLogWebhookToken"] - else: - self.mod_log_webhook_id = None - self.mod_log_webhook_token = None - - # Welcome + Farewell messages - self.welcome = config["Greetings"]["Welcome"].split("\n") - self.goodbye = config["Greetings"]["Goodbye"].split("\n") - - # DB configuration - self.db_path = config["DB"]["Path"] - self.db_schema_path = config["DB"]["Schema"] - - # Helpers configuration - self.course_tpl = config["Helpers"]["CourseTemplate"] - self.course_search_tpl = config["Helpers"]["CourseSearchTemplate"] - self.gc_weather_url = config["Helpers"]["GCWeatherURL"] - self.gc_weather_alert_url = config["Helpers"]["GCWeatherAlertURL"] - self.wttr_in_tpl = config["Helpers"]["WttrINTemplate"] - self.tepid_url = config["Helpers"]["TepidURL"] - - # Subscription configuration - self.recall_channel = config["Subscribers"]["FoodRecallChannel"] - self.recall_filter = config["Subscribers"]["FoodRecallLocationFilter"] - - # Below lies currency configuration - currency_precision = int(config["Currency"]["Precision"]) - - income_tb = zip( - [x.strip() for x in config["IncomeTax"]["Brackets"].split(",")], - [x.strip() for x in config["IncomeTax"]["Amounts"].split(",")], - ) - - asset_tb = zip( - [x.strip() for x in config["AssetTax"]["Brackets"].split(",")], - [x.strip() for x in config["AssetTax"]["Amounts"].split(",")], - ) - - br_cases = zip( - [x.strip() for x in config["Betting"]["RollCases"].split(",")], - [x.strip() for x in config["Betting"]["RollReturns"].split(",")], - ) - - self.currency = { - "name": config["Currency"]["Name"], - "symbol": config["Currency"]["Symbol"], - "precision": currency_precision, - "initial_amount": decimal.Decimal(config["Currency"]["Initial"]), - "salary_base": decimal.Decimal(config["Currency"]["SalaryBase"]), - "inflation": decimal.Decimal(config["Currency"]["Inflation"]), - "income_tax": {decimal.Decimal(b): float(a) for b, a in income_tb}, - "asset_tax": {decimal.Decimal(b): float(a) for b, a in asset_tb}, - "transaction_tax": float(config["OtherTax"]["TransactionTax"]), - "bet_roll_cases": sorted([(int(c), decimal.Decimal(a)) for c, a in br_cases], key=lambda c: c[0]), - } - - self.images = { - "max_image_size": int(config["Images"]["MaxImageSize"]), - "image_history_limit": int(config["Images"]["ImageHistoryLimit"]), - "max_radius": int(config["Images"]["MaxRadius"]), - "max_iterations": int(config["Images"]["MaxIterations"]), - } - - self.games = { - "hm_norm_win": int(config["Games"]["HangmanNormalWin"]), - "hm_cool_win": int(config["Games"]["HangmanCoolWin"]), - "hm_timeout": int(config["Games"]["HangmanTimeOut"]), - } - - # Assignable Roles - roles = { - "pronouns": config["AssignableRoles"]["Pronouns"], - "fields": config["AssignableRoles"]["Fields"], - "faculties": config["AssignableRoles"]["Faculties"], - "years": config["AssignableRoles"]["Years"], - "generics": config["AssignableRoles"]["Generics"], - } - - self.music = {"ban_role": config["Music"]["BanRole"], "start_vol": float(config["Music"]["StartVol"])} - - for rc in roles: - roles[rc] = [r.strip() for r in roles[rc].split(",")] - - self.roles = roles diff --git a/example.env b/example.env new file mode 100644 index 000000000..b315c6bd4 --- /dev/null +++ b/example.env @@ -0,0 +1,8 @@ +# Copy this to .env and enter your customizations! +CANARY_DISCORD_KEY=my_token_here +CANARY_SERVER_ID=test-server-id +CANARY_COMMAND_PREFIX=! +CANARY_DEV_LOG_WEBHOOK_ID= +CANARY_DEV_LOG_WEBHOOK_TOKEN= +CANARY_MOD_LOG_WEBHOOK_ID= +CANARY_MOD_LOG_WEBHOOK_TOKEN= diff --git a/poetry.lock b/poetry.lock index 7f981deb4..62e64a995 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,50 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + [[package]] name = "aiohttp" version = "3.7.4.post0" description = "Async http client/server framework (asyncio)" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, + {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, +] [package.dependencies] async-timeout = ">=3.0,<4.0" @@ -17,95 +57,351 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["aiodns", "brotlipy", "cchardet"] +[[package]] +name = "aiosqlite" +version = "0.19.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, +] + +[package.extras] +dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "async-timeout" version = "3.0.1" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.5.3" - -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] [[package]] name = "attrs" -version = "22.1.0" +version = "24.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "beautifulsoup4" -version = "4.10.0" +version = "4.12.3" description = "Screen-scraping library" -category = "main" optional = false -python-versions = ">3.0.0" +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] [package.dependencies] soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] [[package]] name = "bidict" -version = "0.22.0" +version = "0.22.1" description = "The bidirectional mapping library for Python." -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "bidict-0.22.1-py3-none-any.whl", hash = "sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b"}, + {file = "bidict-0.22.1.tar.gz", hash = "sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton"] +lint = ["pre-commit"] +test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "pytest-xdist", "sortedcollections", "sortedcontainers", "sphinx"] [[package]] name = "black" -version = "22.6.0" +version = "24.8.0" description = "The uncompromising code formatter." -category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" + [[package]] name = "certifi" -version = "2022.6.15" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] [[package]] name = "cffi" -version = "1.15.1" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] [package.dependencies] pycparser = "*" @@ -114,47 +410,147 @@ pycparser = "*" name = "chardet" version = "3.0.4" description = "Universal encoding detector for Python 2 and 3" -category = "main" optional = false python-versions = "*" +files = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] -name = "discord.py" -version = "1.6.0" +name = "discord-py" +version = "1.7.3" description = "A Python wrapper for the Discord API" -category = "main" optional = false python-versions = ">=3.5.3" +files = [ + {file = "discord.py-1.7.3-py3-none-any.whl", hash = "sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c"}, + {file = "discord.py-1.7.3.tar.gz", hash = "sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408"}, +] [package.dependencies] aiohttp = ">=3.6.0,<3.8.0" @@ -164,13 +560,30 @@ PyNaCl = {version = ">=1.3.0,<1.5", optional = true, markers = "extra == \"voice docs = ["sphinx (==3.0.3)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"] voice = ["PyNaCl (>=1.3.0,<1.5)"] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "feedparser" -version = "6.0.10" +version = "6.0.11" description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45"}, + {file = "feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5"}, +] [package.dependencies] sgmllib3k = "*" @@ -179,9 +592,11 @@ sgmllib3k = "*" name = "googletrans" version = "4.0.0rc1" description = "Free Google Translate API for Python. Translates totally free of charge." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "googletrans-4.0.0rc1.tar.gz", hash = "sha256:74df47b092e2d566522019d149e3f1d75732570ad76eaf8e14aebeffc126c372"}, +] [package.dependencies] httpx = "0.13.3" @@ -190,17 +605,23 @@ httpx = "0.13.3" name = "h11" version = "0.9.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = "*" +files = [ + {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, + {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, +] [[package]] name = "h2" version = "3.2.0" description = "HTTP/2 State-Machine based protocol implementation" -category = "main" optional = false python-versions = "*" +files = [ + {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, + {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, +] [package.dependencies] hpack = ">=3.0,<4" @@ -210,45 +631,57 @@ hyperframe = ">=5.2.0,<6" name = "hpack" version = "3.0.0" description = "Pure-Python HPACK header compression" -category = "main" optional = false python-versions = "*" +files = [ + {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, + {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, +] [[package]] name = "hstspreload" -version = "2022.8.1" +version = "2024.9.1" description = "Chromium HSTS Preload list as a Python package" -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "hstspreload-2024.9.1-py3-none-any.whl", hash = "sha256:9c1b2d0313899d3ff9dac03ab39d53fed95c32eef9862e7eabee8dc07dfd589c"}, + {file = "hstspreload-2024.9.1.tar.gz", hash = "sha256:2ab4518495a132c4ae430c474afffd12938852c6eec68ce1368c8f7858dc3076"}, +] [[package]] name = "httpcore" version = "0.9.1" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, + {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"}, +] [package.dependencies] h11 = ">=0.8,<0.10" -h2 = ">=3.0.0,<4.0.0" -sniffio = ">=1.0.0,<2.0.0" +h2 = "==3.*" +sniffio = "==1.*" [[package]] name = "httpx" version = "0.13.3" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"}, + {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"}, +] [package.dependencies] certifi = "*" -chardet = ">=3.0.0,<4.0.0" +chardet = "==3.*" hstspreload = "*" -httpcore = ">=0.9.0,<0.10.0" -idna = ">=2.0.0,<3.0.0" +httpcore = "==0.9.*" +idna = "==2.*" rfc3986 = ">=1.3,<2" sniffio = "*" @@ -256,168 +689,665 @@ sniffio = "*" name = "hyperframe" version = "5.2.0" description = "HTTP/2 framing layer for Python" -category = "main" optional = false python-versions = "*" +files = [ + {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, + {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, +] [[package]] name = "idna" version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "lxml" +version = "4.9.4" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722"}, + {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1"}, + {file = "lxml-4.9.4-cp27-cp27m-win32.whl", hash = "sha256:7d1d6c9e74c70ddf524e3c09d9dc0522aba9370708c2cb58680ea40174800013"}, + {file = "lxml-4.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:cb53669442895763e61df5c995f0e8361b61662f26c1b04ee82899c2789c8f69"}, + {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:647bfe88b1997d7ae8d45dabc7c868d8cb0c8412a6e730a7651050b8c7289cf2"}, + {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4d973729ce04784906a19108054e1fd476bc85279a403ea1a72fdb051c76fa48"}, + {file = "lxml-4.9.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:056a17eaaf3da87a05523472ae84246f87ac2f29a53306466c22e60282e54ff8"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aaa5c173a26960fe67daa69aa93d6d6a1cd714a6eb13802d4e4bd1d24a530644"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:647459b23594f370c1c01768edaa0ba0959afc39caeeb793b43158bb9bb6a663"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bdd9abccd0927673cffe601d2c6cdad1c9321bf3437a2f507d6b037ef91ea307"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:00e91573183ad273e242db5585b52670eddf92bacad095ce25c1e682da14ed91"}, + {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a602ed9bd2c7d85bd58592c28e101bd9ff9c718fbde06545a70945ffd5d11868"}, + {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de362ac8bc962408ad8fae28f3967ce1a262b5d63ab8cefb42662566737f1dc7"}, + {file = "lxml-4.9.4-cp310-cp310-win32.whl", hash = "sha256:33714fcf5af4ff7e70a49731a7cc8fd9ce910b9ac194f66eaa18c3cc0a4c02be"}, + {file = "lxml-4.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:d3caa09e613ece43ac292fbed513a4bce170681a447d25ffcbc1b647d45a39c5"}, + {file = "lxml-4.9.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:359a8b09d712df27849e0bcb62c6a3404e780b274b0b7e4c39a88826d1926c28"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:43498ea734ccdfb92e1886dfedaebeb81178a241d39a79d5351ba2b671bff2b2"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4855161013dfb2b762e02b3f4d4a21cc7c6aec13c69e3bffbf5022b3e708dd97"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c71b5b860c5215fdbaa56f715bc218e45a98477f816b46cfde4a84d25b13274e"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9a2b5915c333e4364367140443b59f09feae42184459b913f0f41b9fed55794a"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d82411dbf4d3127b6cde7da0f9373e37ad3a43e89ef374965465928f01c2b979"}, + {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:273473d34462ae6e97c0f4e517bd1bf9588aa67a1d47d93f760a1282640e24ac"}, + {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:389d2b2e543b27962990ab529ac6720c3dded588cc6d0f6557eec153305a3622"}, + {file = "lxml-4.9.4-cp311-cp311-win32.whl", hash = "sha256:8aecb5a7f6f7f8fe9cac0bcadd39efaca8bbf8d1bf242e9f175cbe4c925116c3"}, + {file = "lxml-4.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:c7721a3ef41591341388bb2265395ce522aba52f969d33dacd822da8f018aff8"}, + {file = "lxml-4.9.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:dbcb2dc07308453db428a95a4d03259bd8caea97d7f0776842299f2d00c72fc8"}, + {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01bf1df1db327e748dcb152d17389cf6d0a8c5d533ef9bab781e9d5037619229"}, + {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e8f9f93a23634cfafbad6e46ad7d09e0f4a25a2400e4a64b1b7b7c0fbaa06d9d"}, + {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3f3f00a9061605725df1816f5713d10cd94636347ed651abdbc75828df302b20"}, + {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:953dd5481bd6252bd480d6ec431f61d7d87fdcbbb71b0d2bdcfc6ae00bb6fb10"}, + {file = "lxml-4.9.4-cp312-cp312-win32.whl", hash = "sha256:266f655d1baff9c47b52f529b5f6bec33f66042f65f7c56adde3fcf2ed62ae8b"}, + {file = "lxml-4.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:f1faee2a831fe249e1bae9cbc68d3cd8a30f7e37851deee4d7962b17c410dd56"}, + {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23d891e5bdc12e2e506e7d225d6aa929e0a0368c9916c1fddefab88166e98b20"}, + {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e96a1788f24d03e8d61679f9881a883ecdf9c445a38f9ae3f3f193ab6c591c66"}, + {file = "lxml-4.9.4-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:5557461f83bb7cc718bc9ee1f7156d50e31747e5b38d79cf40f79ab1447afd2d"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:fdb325b7fba1e2c40b9b1db407f85642e32404131c08480dd652110fc908561b"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d74d4a3c4b8f7a1f676cedf8e84bcc57705a6d7925e6daef7a1e54ae543a197"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ac7674d1638df129d9cb4503d20ffc3922bd463c865ef3cb412f2c926108e9a4"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:ddd92e18b783aeb86ad2132d84a4b795fc5ec612e3545c1b687e7747e66e2b53"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bd9ac6e44f2db368ef8986f3989a4cad3de4cd55dbdda536e253000c801bcc7"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bc354b1393dce46026ab13075f77b30e40b61b1a53e852e99d3cc5dd1af4bc85"}, + {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f836f39678cb47c9541f04d8ed4545719dc31ad850bf1832d6b4171e30d65d23"}, + {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9c131447768ed7bc05a02553d939e7f0e807e533441901dd504e217b76307745"}, + {file = "lxml-4.9.4-cp36-cp36m-win32.whl", hash = "sha256:bafa65e3acae612a7799ada439bd202403414ebe23f52e5b17f6ffc2eb98c2be"}, + {file = "lxml-4.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6197c3f3c0b960ad033b9b7d611db11285bb461fc6b802c1dd50d04ad715c225"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:7b378847a09d6bd46047f5f3599cdc64fcb4cc5a5a2dd0a2af610361fbe77b16"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:1343df4e2e6e51182aad12162b23b0a4b3fd77f17527a78c53f0f23573663545"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6dbdacf5752fbd78ccdb434698230c4f0f95df7dd956d5f205b5ed6911a1367c"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:506becdf2ecaebaf7f7995f776394fcc8bd8a78022772de66677c84fb02dd33d"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca8e44b5ba3edb682ea4e6185b49661fc22b230cf811b9c13963c9f982d1d964"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9d9d5726474cbbef279fd709008f91a49c4f758bec9c062dfbba88eab00e3ff9"}, + {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bbdd69e20fe2943b51e2841fc1e6a3c1de460d630f65bde12452d8c97209464d"}, + {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8671622256a0859f5089cbe0ce4693c2af407bc053dcc99aadff7f5310b4aa02"}, + {file = "lxml-4.9.4-cp37-cp37m-win32.whl", hash = "sha256:dd4fda67f5faaef4f9ee5383435048ee3e11ad996901225ad7615bc92245bc8e"}, + {file = "lxml-4.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6bee9c2e501d835f91460b2c904bc359f8433e96799f5c2ff20feebd9bb1e590"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:1f10f250430a4caf84115b1e0f23f3615566ca2369d1962f82bef40dd99cd81a"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b505f2bbff50d261176e67be24e8909e54b5d9d08b12d4946344066d66b3e43"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1449f9451cd53e0fd0a7ec2ff5ede4686add13ac7a7bfa6988ff6d75cff3ebe2"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4ece9cca4cd1c8ba889bfa67eae7f21d0d1a2e715b4d5045395113361e8c533d"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59bb5979f9941c61e907ee571732219fa4774d5a18f3fa5ff2df963f5dfaa6bc"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b1980dbcaad634fe78e710c8587383e6e3f61dbe146bcbfd13a9c8ab2d7b1192"}, + {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9ae6c3363261021144121427b1552b29e7b59de9d6a75bf51e03bc072efb3c37"}, + {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bcee502c649fa6351b44bb014b98c09cb00982a475a1912a9881ca28ab4f9cd9"}, + {file = "lxml-4.9.4-cp38-cp38-win32.whl", hash = "sha256:a8edae5253efa75c2fc79a90068fe540b197d1c7ab5803b800fccfe240eed33c"}, + {file = "lxml-4.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:701847a7aaefef121c5c0d855b2affa5f9bd45196ef00266724a80e439220e46"}, + {file = "lxml-4.9.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:f610d980e3fccf4394ab3806de6065682982f3d27c12d4ce3ee46a8183d64a6a"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aa9b5abd07f71b081a33115d9758ef6077924082055005808f68feccb27616bd"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:365005e8b0718ea6d64b374423e870648ab47c3a905356ab6e5a5ff03962b9a9"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:16b9ec51cc2feab009e800f2c6327338d6ee4e752c76e95a35c4465e80390ccd"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a905affe76f1802edcac554e3ccf68188bea16546071d7583fb1b693f9cf756b"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd814847901df6e8de13ce69b84c31fc9b3fb591224d6762d0b256d510cbf382"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91bbf398ac8bb7d65a5a52127407c05f75a18d7015a270fdd94bbcb04e65d573"}, + {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f99768232f036b4776ce419d3244a04fe83784bce871b16d2c2e984c7fcea847"}, + {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bb5bd6212eb0edfd1e8f254585290ea1dadc3687dd8fd5e2fd9a87c31915cdab"}, + {file = "lxml-4.9.4-cp39-cp39-win32.whl", hash = "sha256:88f7c383071981c74ec1998ba9b437659e4fd02a3c4a4d3efc16774eb108d0ec"}, + {file = "lxml-4.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:936e8880cc00f839aa4173f94466a8406a96ddce814651075f95837316369899"}, + {file = "lxml-4.9.4-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:f6c35b2f87c004270fa2e703b872fcc984d714d430b305145c39d53074e1ffe0"}, + {file = "lxml-4.9.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:606d445feeb0856c2b424405236a01c71af7c97e5fe42fbc778634faef2b47e4"}, + {file = "lxml-4.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1bdcbebd4e13446a14de4dd1825f1e778e099f17f79718b4aeaf2403624b0f7"}, + {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0a08c89b23117049ba171bf51d2f9c5f3abf507d65d016d6e0fa2f37e18c0fc5"}, + {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:232fd30903d3123be4c435fb5159938c6225ee8607b635a4d3fca847003134ba"}, + {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:231142459d32779b209aa4b4d460b175cadd604fed856f25c1571a9d78114771"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:520486f27f1d4ce9654154b4494cf9307b495527f3a2908ad4cb48e4f7ed7ef7"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:562778586949be7e0d7435fcb24aca4810913771f845d99145a6cee64d5b67ca"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a9e7c6d89c77bb2770c9491d988f26a4b161d05c8ca58f63fb1f1b6b9a74be45"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:786d6b57026e7e04d184313c1359ac3d68002c33e4b1042ca58c362f1d09ff58"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95ae6c5a196e2f239150aa4a479967351df7f44800c93e5a975ec726fef005e2"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:9b556596c49fa1232b0fff4b0e69b9d4083a502e60e404b44341e2f8fb7187f5"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cc02c06e9e320869d7d1bd323df6dd4281e78ac2e7f8526835d3d48c69060683"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:857d6565f9aa3464764c2cb6a2e3c2e75e1970e877c188f4aeae45954a314e0c"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c42ae7e010d7d6bc51875d768110c10e8a59494855c3d4c348b068f5fb81fdcd"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f10250bb190fb0742e3e1958dd5c100524c2cc5096c67c8da51233f7448dc137"}, + {file = "lxml-4.9.4.tar.gz", hash = "sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (==0.29.37)"] [[package]] name = "mpmath" -version = "1.2.1" +version = "1.3.0" description = "Python library for arbitrary-precision floating-point arithmetic" -category = "main" optional = false python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] [package.extras] develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] tests = ["pytest (>=4.6)"] [[package]] name = "multidict" -version = "6.0.2" +version = "6.0.5" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] [[package]] name = "mutagen" -version = "1.45.1" +version = "1.47.0" description = "read and write audio tags for many formats" -category = "main" optional = false -python-versions = ">=3.5, <4" +python-versions = ">=3.7" +files = [ + {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, + {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, +] + +[[package]] +name = "mypy" +version = "1.11.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] [[package]] name = "numpy" -version = "1.23.2" -description = "NumPy is the fundamental package for array computing with Python." -category = "main" +version = "1.26.4" +description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] [[package]] name = "opencv-python" -version = "4.6.0.66" +version = "4.10.0.84" description = "Wrapper package for OpenCV python bindings." -category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, +] [package.dependencies] numpy = [ - {version = ">=1.21.2", markers = "python_version >= \"3.10\" or python_version >= \"3.6\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.14.5", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, ] [[package]] name = "packaging" -version = "21.3" +version = "24.1" description = "Core utilities for Python packages" -category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] [[package]] name = "pathspec" -version = "0.9.0" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] [[package]] name = "pillow" -version = "8.4.0" +version = "9.5.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" +version = "4.3.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.1-py3-none-any.whl", hash = "sha256:facaa5a3c57aa1e053e3da7b49e0cc31fe0113ca42a4659d5c2e98e545624afe"}, + {file = "platformdirs-4.3.1.tar.gz", hash = "sha256:63b79589009fa8159973601dd4563143396b35c5f93a58b36f9049ff046949b1"}, +] [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] [[package]] name = "pycryptodomex" -version = "3.15.0" +version = "3.20.0" description = "Cryptographic library for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, + {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, +] + +[[package]] +name = "pydantic" +version = "1.10.18" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"}, + {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"}, + {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"}, + {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"}, + {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"}, + {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"}, + {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"}, + {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"}, + {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"}, + {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pynacl" version = "1.4.0" description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] [package.dependencies] cffi = ">=1.4.1" @@ -427,79 +1357,186 @@ six = "*" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pytest" -version = "7.1.2" +version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dotenv" +version = "0.21.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"}, + {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"}, +] [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2022.2.1" +version = "2024.1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] [[package]] name = "regex" -version = "2021.11.10" +version = "2023.12.25" description = "Alternative regular expression module, to replace re." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, +] [[package]] name = "requests" -version = "2.28.1" +version = "2.32.3" description = "Python HTTP for Humans." -category = "main" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" -category = "main" optional = false python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] [package.extras] idna2008 = ["idna"] @@ -508,52 +1545,72 @@ idna2008 = ["idna"] name = "sgmllib3k" version = "1.0.0" description = "Py3k port of sgmllib." -category = "main" optional = false python-versions = "*" +files = [ + {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, +] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "sniffio" -version = "1.2.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] [[package]] name = "soupsieve" -version = "2.3.2.post1" +version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] [[package]] name = "sympy" -version = "1.11" +version = "1.13.2" description = "Computer algebra system (CAS) in Python" -category = "main" optional = false python-versions = ">=3.8" +files = [ + {file = "sympy-1.13.2-py3-none-any.whl", hash = "sha256:c51d75517712f1aed280d4ce58506a4a88d635d6b5dd48b39102a7ae1f3fcfe9"}, + {file = "sympy-1.13.2.tar.gz", hash = "sha256:401449d84d07be9d0c7a46a64bd54fe097667d5e7181bfe67ec777be9e01cb13"}, +] [package.dependencies] -mpmath = ">=0.19" +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] name = "tabulate" -version = "0.8.9" +version = "0.8.10" description = "Pretty-print tabular data" -category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc"}, + {file = "tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519"}, +] [package.extras] widechars = ["wcwidth"] @@ -562,59 +1619,352 @@ widechars = ["wcwidth"] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20240907" +description = "Typing stubs for beautifulsoup4" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-beautifulsoup4-4.12.0.20240907.tar.gz", hash = "sha256:8d023b86530922070417a1d4c4d91678ab0ff2439b3b2b2cffa3b628b49ebab1"}, + {file = "types_beautifulsoup4-4.12.0.20240907-py3-none-any.whl", hash = "sha256:32f5ac48514b488f15241afdd7d2f73f0baf3c54e874e23b66708503dd288489"}, +] + +[package.dependencies] +types-html5lib = "*" + +[[package]] +name = "types-html5lib" +version = "1.1.11.20240806" +description = "Typing stubs for html5lib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-html5lib-1.1.11.20240806.tar.gz", hash = "sha256:8060dc98baf63d6796a765bbbc809fff9f7a383f6e3a9add526f814c086545ef"}, + {file = "types_html5lib-1.1.11.20240806-py3-none-any.whl", hash = "sha256:575c4fd84ba8eeeaa8520c7e4c7042b7791f5ec3e9c0a5d5c418124c42d9e7e4"}, +] + +[[package]] +name = "types-pytz" +version = "2024.1.0.20240417" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.1.0.20240417.tar.gz", hash = "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981"}, + {file = "types_pytz-2024.1.0.20240417-py3-none-any.whl", hash = "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659"}, +] + +[[package]] +name = "types-regex" +version = "2024.7.24.20240726" +description = "Typing stubs for regex" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-regex-2024.7.24.20240726.tar.gz", hash = "sha256:f9cbebe607f53860bf5979de1e2a80cc04faf4849ee324461f982a3d46276d76"}, + {file = "types_regex-2024.7.24.20240726-py3-none-any.whl", hash = "sha256:c436d7eace8e6c33cec31630135c804c15cd4ef110baf9cdd370ac6e376ff661"}, +] + +[[package]] +name = "types-requests" +version = "2.32.0.20240907" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.32.0.20240907.tar.gz", hash = "sha256:ff33935f061b5e81ec87997e91050f7b4af4f82027a7a7a9d9aaea04a963fdf8"}, + {file = "types_requests-2.32.0.20240907-py3-none-any.whl", hash = "sha256:1d1e79faeaf9d42def77f3c304893dea17a97cae98168ac69f3cb465516ee8da"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "types-tabulate" +version = "0.8.11" +description = "Typing stubs for tabulate" +optional = false +python-versions = "*" +files = [ + {file = "types-tabulate-0.8.11.tar.gz", hash = "sha256:17a5fa3b5ca453815778fc9865e8ecd0118b07b2b9faff3e2b06fe448174dd5e"}, + {file = "types_tabulate-0.8.11-py3-none-any.whl", hash = "sha256:af811268241e8fb87b63c052c87d1e329898a93191309d5d42111372232b2e0e"}, +] [[package]] name = "typing-extensions" -version = "4.3.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +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"}, +] [[package]] name = "urllib3" -version = "1.26.12" +version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvloop" -version = "0.16.0" +version = "0.20.0" description = "Fast implementation of asyncio event loop on top of libuv" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, + {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, + {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10"}, + {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae"}, + {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006"}, + {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73"}, + {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037"}, + {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9"}, + {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e"}, + {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756"}, + {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0"}, + {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf"}, + {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d"}, + {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e"}, + {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9"}, + {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab"}, + {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5"}, + {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00"}, + {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba"}, + {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf"}, + {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847"}, + {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b"}, + {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6"}, + {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2"}, + {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95"}, + {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7"}, + {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a"}, + {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541"}, + {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315"}, + {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66"}, + {file = "uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469"}, +] [package.extras] -dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] [[package]] name = "websockets" -version = "10.3" +version = "13.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f"}, + {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c"}, + {file = "websockets-13.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f"}, + {file = "websockets-13.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543"}, + {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d"}, + {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f"}, + {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8"}, + {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b"}, + {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448"}, + {file = "websockets-13.0.1-cp310-cp310-win32.whl", hash = "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3"}, + {file = "websockets-13.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0"}, + {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7"}, + {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4"}, + {file = "websockets-13.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2"}, + {file = "websockets-13.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0"}, + {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e"}, + {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462"}, + {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501"}, + {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418"}, + {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df"}, + {file = "websockets-13.0.1-cp311-cp311-win32.whl", hash = "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f"}, + {file = "websockets-13.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075"}, + {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a"}, + {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956"}, + {file = "websockets-13.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af"}, + {file = "websockets-13.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf"}, + {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c"}, + {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4"}, + {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab"}, + {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d"}, + {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237"}, + {file = "websockets-13.0.1-cp312-cp312-win32.whl", hash = "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185"}, + {file = "websockets-13.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99"}, + {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa"}, + {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231"}, + {file = "websockets-13.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9"}, + {file = "websockets-13.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75"}, + {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553"}, + {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920"}, + {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329"}, + {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7"}, + {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2"}, + {file = "websockets-13.0.1-cp313-cp313-win32.whl", hash = "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb"}, + {file = "websockets-13.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b"}, + {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e"}, + {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2"}, + {file = "websockets-13.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b"}, + {file = "websockets-13.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae"}, + {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f"}, + {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603"}, + {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36"}, + {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a"}, + {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb"}, + {file = "websockets-13.0.1-cp38-cp38-win32.whl", hash = "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4"}, + {file = "websockets-13.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9"}, + {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc"}, + {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63"}, + {file = "websockets-13.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37"}, + {file = "websockets-13.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9"}, + {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97"}, + {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f"}, + {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4"}, + {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0"}, + {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d"}, + {file = "websockets-13.0.1-cp39-cp39-win32.whl", hash = "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58"}, + {file = "websockets-13.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980"}, + {file = "websockets-13.0.1-py3-none-any.whl", hash = "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817"}, + {file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"}, +] [[package]] name = "yarl" -version = "1.8.1" +version = "1.10.0" description = "Yet another URL library" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "yarl-1.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1718c0bca5a61edac7a57dcc11856cb01bde13a9360a3cb6baf384b89cfc0b40"}, + {file = "yarl-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4657fd290d556a5f3018d07c7b7deadcb622760c0125277d10a11471c340054"}, + {file = "yarl-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:044b76d069e69c6b0246f071ebac0576f89c772f806d66ef51e662bd015d03c7"}, + {file = "yarl-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5527d32506c11150ca87f33820057dc284e2a01a87f0238555cada247a8b278"}, + {file = "yarl-1.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36d12d78b8b0d46099d413c8689b5510ad9ce5e443363d1c37b6ac5b3d7cbdfb"}, + {file = "yarl-1.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11f7f8a72b3e26c533fa7ffa7a8068f4e3aad7b67c5cf7b17ea8c79fc81d9830"}, + {file = "yarl-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88173836a25b7e5dce989eeee3b92d8ef5cdf512830d4155c6212de98e616f70"}, + {file = "yarl-1.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c382e189af10070bcb39caa9406b9cc47b26c1d2257979f11fe03a38be09fea9"}, + {file = "yarl-1.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:534b8bc181dca1691cf491c263e084af678a8fb6b6181687c788027d8c317026"}, + {file = "yarl-1.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f3372f9ae1d1f001826b77d0b29d4220e84f6c5f53915e71a825cdd02600065"}, + {file = "yarl-1.10.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cca9ba00be4bb8a051c4007b60fc91d6c9728c8b70c86cee4c24be9d641002f"}, + {file = "yarl-1.10.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a9d8c4be5658834dc688072239d220631ad4b71ff79a5f3d17fb653f16d10759"}, + {file = "yarl-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff45a655ca51e1cb778abbb586083fddb7d896332f47bb3b03bc75e30c25649f"}, + {file = "yarl-1.10.0-cp310-cp310-win32.whl", hash = "sha256:9ef7ce61958b3c7b2e2e0927c52d35cf367c5ee410e06e1337ecc83a90c23b95"}, + {file = "yarl-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:48a48261f8d610b0e15fed033e74798763bc2f8f2c0d769a2a0732511af71f1e"}, + {file = "yarl-1.10.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:308d1cce071b5b500e3d95636bbf15dfdb8e87ed081b893555658a7f9869a156"}, + {file = "yarl-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc66927f6362ed613a483c22618f88f014994ccbd0b7a25ec1ebc8c472d4b40a"}, + {file = "yarl-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c4d13071c5b99974cfe2f94c749ecc4baf882f7c4b6e4c40ca3d15d1b7e81f24"}, + {file = "yarl-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:348ad53acd41caa489df7db352d620c982ab069855d9635dda73d685bbbc3636"}, + {file = "yarl-1.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:293f7c2b30d015de3f1441c4ee764963b86636fde881b4d6093498d1e8711f69"}, + {file = "yarl-1.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:315e8853d0ea46aabdce01f1f248fff7b9743de89b555c5f0487f54ac84beae8"}, + {file = "yarl-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:012c506b2c23be4500fb97509aa7e6a575996fb317b80667fa26899d456e2aaf"}, + {file = "yarl-1.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f769c2708c31227c5349c3e4c668c8b4b2e25af3e7263723f2ef33e8e3906a0"}, + {file = "yarl-1.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f6ac063a4e9bbd4f6cc88cc621516a44d6aec66862ea8399ba063374e4b12c7"}, + {file = "yarl-1.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:18b7ce6d8c35da8e16dcc8de124a80e250fc8c73f8c02663acf2485c874f1972"}, + {file = "yarl-1.10.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b80246bdee036381636e73ef0f19b032912064622b0e5ee44f6960fd11df12aa"}, + {file = "yarl-1.10.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:183dd37bb5471e8017ab8a998c1ea070b4a0b08a97a7c4e20e0c7ccbe8ebb999"}, + {file = "yarl-1.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b6d0d7522b514f054b359409817af4c5ed76fa4fe42d8bd1ed12956804cf595"}, + {file = "yarl-1.10.0-cp311-cp311-win32.whl", hash = "sha256:6026a6ef14d038a38ca9d81422db4b6bb7d5da94f9d08f21e0ad9ebd9c4bc3bb"}, + {file = "yarl-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:190e70d2f9f16f1c9d666c103d635c9ed4bf8de7803e9fa0495eec405a3e96a8"}, + {file = "yarl-1.10.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6bc602c7413e1b5223bc988947125998cb54d6184de45a871985daacc23e6c8c"}, + {file = "yarl-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bf733c835ebbd52bd78a52b919205e0f06d8571f71976a0259e5bcc20d0a2f44"}, + {file = "yarl-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e91ed5f6818e1e3806eaeb7b14d9e17b90340f23089451ea59a89a29499d760"}, + {file = "yarl-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23057a004bc9735008eb2a04b6ce94c6c06219cdf2b193997fd3ae6039eb3196"}, + {file = "yarl-1.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b922c32a1cff62bc43d408d1a8745abeed0a705793f2253c622bf3521922198"}, + {file = "yarl-1.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be199fed28861d72df917e355287ad6835555d8210e7f8203060561f24d7d842"}, + {file = "yarl-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cece693380c1c4a606cdcaa0c54eda8f72cfe1ba83f5149b9023bb955e8fa8e"}, + {file = "yarl-1.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff8e803d8ca170e632fb3b4df1bfd29ba29be8edc3e9306c5ffa5fadea234a4f"}, + {file = "yarl-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:30dde3a8b88c80a4f049eb4dd240d2a02e89174da6be2525541f949bf9fa38ab"}, + {file = "yarl-1.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dff84623e7098cf9bfbb5187f9883051af652b0ce08b9f7084cc8630b87b6457"}, + {file = "yarl-1.10.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e69b55965a47dd6c79e578abd7d85637b1bb4a7565436630826bdb28aa9b7ad"}, + {file = "yarl-1.10.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5d0c9e1dcc92d46ca89608fe4763fc2362f1e81c19a922c67dbc0f20951466e4"}, + {file = "yarl-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32e79d5ae975f7c2cc29f7104691fc9be5ee3724f24e1a7254d72f6219672108"}, + {file = "yarl-1.10.0-cp312-cp312-win32.whl", hash = "sha256:762a196612c2aba4197cd271da65fe08308f7ddf130dc63842c7a76d774b6a2c"}, + {file = "yarl-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:8c6214071f653d21bb7b43f7ee519afcbf7084263bb43408f4939d14558290db"}, + {file = "yarl-1.10.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0e0aea8319fdc1ac340236e58b0b7dc763621bce6ce98124a9d58104cafd0aaa"}, + {file = "yarl-1.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b3bf343b4ef9ec600d75363eb9b48ab3bd53b53d4e1c5a9fbf0cfe7ba73a47f"}, + {file = "yarl-1.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:05b07e6e0f715eaae9d927a302d9220724392f3c0b4e7f8dfa174bf2e1b8433e"}, + {file = "yarl-1.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7bd531d7eec4aa7ef8a99fef91962eeea5158a53af0ec507c476ddf8ebc29c"}, + {file = "yarl-1.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:183136dc5d5411872e7529c924189a2e26fac5a7f9769cf13ef854d1d653ad36"}, + {file = "yarl-1.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c77a3c10af4aaf8891578fe492ef0990c65cf7005dd371f5ea8007b420958bf6"}, + {file = "yarl-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:030d41d48217b180c5a176e59c49d212d54d89f6f53640fa4c1a1766492aec27"}, + {file = "yarl-1.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4f43ba30d604ba391bc7fe2dd104d6b87b62b0de4bbde79e362524b8a1eb75"}, + {file = "yarl-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:637dd0f55d1781d4634c23994101c509e455b5ab61af9086b5763b7eca9359aa"}, + {file = "yarl-1.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:99e7459ee86a3b81e57777afd3825b8b1acaac8a99f9c0bd02415d80eb3c371b"}, + {file = "yarl-1.10.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a80cdb3c15c15b33ecdb080546dcb022789b0084ca66ad41ffa0fe09857fca11"}, + {file = "yarl-1.10.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1824bfb932d8100e5c94f4f98c078f23ebc6f6fa93acc3d95408762089c54a06"}, + {file = "yarl-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:90fd64ce00f594db02f603efa502521c440fa1afcf6266be82eb31f19d2d9561"}, + {file = "yarl-1.10.0-cp313-cp313-win32.whl", hash = "sha256:687131ee4d045f3d58128ca28f5047ec902f7760545c39bbe003cc737c5a02b5"}, + {file = "yarl-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:493ad061ee025c5ed3a60893cd70204eead1b3f60ccc90682e752f95b845bd46"}, + {file = "yarl-1.10.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cd65588273d19f8483bc8f32a6fcf602e94a9a7ba287a1725977bd9527cd6c0c"}, + {file = "yarl-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6f64f8681671624f539eea5564518bc924524c25eb90ab24a7eddc2d872e668e"}, + {file = "yarl-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3576ed2c51f8525d4ff5c3279247aacff9540bb43b292c4a37a8e6c6e1691adb"}, + {file = "yarl-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca42a9281807fdf8fba86e671d8fdd76f92e9302a6d332957f2bae51c774f8a7"}, + {file = "yarl-1.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54a4b5e6a060d46cad6a3cf340f4cb268e6fbc89c589d82a2da58f7db47c47c8"}, + {file = "yarl-1.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eec21d8c3aa932c5a89480b58fa877e9c48092ab838ccc76788cbc917ceec0d"}, + {file = "yarl-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273baee8a8af5989d5aab51c740e65bc2b1fc6619b9dd192cd16a3fae51100be"}, + {file = "yarl-1.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1bf63ba496cd4f12d30e916d9a52daa6c91433fedd9cd0d99fef3e13232836f"}, + {file = "yarl-1.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f8e24b9a4afdffab399191a9f0b0e80eabc7b7fdb9f2dbccdeb8e4d28e5c57bb"}, + {file = "yarl-1.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4c46454fafa31f7241083a0dd21814f63e0fcb4ae49662dc7e286fd6a5160ea1"}, + {file = "yarl-1.10.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:beda87b63c08fb4df8cc5353eeefe68efe12aa4f5284958bd1466b14c85e508e"}, + {file = "yarl-1.10.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9a8d6a0e2b5617b5c15c59db25f20ba429f1fea810f2c09fbf93067cb21ab085"}, + {file = "yarl-1.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b453b3dbc1ed4c2907632d05b378123f3fb411cad05d8d96de7d95104ef11c70"}, + {file = "yarl-1.10.0-cp38-cp38-win32.whl", hash = "sha256:1ea30675fbf0ad6795c100da677ef6a8960a7db05ac5293f02a23c2230203c89"}, + {file = "yarl-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:347011ad09a8f9be3d41fe2d7d611c3a4de4d49aa77bcb9a8c03c7a82fc45248"}, + {file = "yarl-1.10.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:18bc4600eed1907762c1816bb16ac63bc52912e53b5e9a353eb0935a78e95496"}, + {file = "yarl-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeb6a40c5ae2616fd38c1e039c6dd50031bbfbc2acacfd7b70a5d64fafc70901"}, + {file = "yarl-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc544248b5263e1c0f61332ccf35e37404b54213f77ed17457f857f40af51452"}, + {file = "yarl-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3352c69dc235850d6bf8ddad915931f00dcab208ac4248b9af46175204c2f5f9"}, + {file = "yarl-1.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af5b52bfbbd5eb208cf1afe23c5ada443929e9b9d79e9fbc66cacc07e4e39748"}, + {file = "yarl-1.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eafa7317063de4bc310716cdd9026c13f00b1629e649079a6908c3aafdf5046"}, + {file = "yarl-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a162cf04fd1e8d81025ec651d14cac4f6e0ca73a3c0a9482de8691b944e3098a"}, + {file = "yarl-1.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:179b1df5e9cd99234ea65e63d5bfc6dd524b2c3b6cf68a14b94ccbe01ab37ddd"}, + {file = "yarl-1.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:32d2e46848dea122484317485129f080220aa84aeb6a9572ad9015107cebeb07"}, + {file = "yarl-1.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aa1aeb99408be0ca774c5126977eb085fedda6dd7d9198ce4ceb2d06a44325c7"}, + {file = "yarl-1.10.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d2366e2f987f69752f0588d2035321aaf24272693d75f7f6bb7e8a0f48f7ccdd"}, + {file = "yarl-1.10.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e8da33665ecc64cd3e593098adb449f9c65b4e3bc6338e75ad592da15453d898"}, + {file = "yarl-1.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b46c603bee1f2dd407b8358c2afc9b0472a22ccca528f114e1f4cd30dfecd22"}, + {file = "yarl-1.10.0-cp39-cp39-win32.whl", hash = "sha256:96422a3322b4d954f4c52403a2fc129ad118c151ee60a717847fb46a8480d1e1"}, + {file = "yarl-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:52d1ae09b0764017e330bb5bf9af760c0168c564225085bb806f687bccffda8a"}, + {file = "yarl-1.10.0-py3-none-any.whl", hash = "sha256:99eaa7d53f509ba1c2fea8fdfec15ba3cd36caca31d57ec6665073b148b5f260"}, + {file = "yarl-1.10.0.tar.gz", hash = "sha256:3bf10a395adac62177ba8ea738617e8de6cbb1cea6aa5d5dd2accde704fc8195"}, +] [package.dependencies] idna = ">=2.0" @@ -622,417 +1972,36 @@ multidict = ">=4.0" [[package]] name = "yt-dlp" -version = "2021.12.27" -description = "A youtube-dl fork with additional features and patches" -category = "main" +version = "2024.8.6" +description = "A feature-rich command-line audio/video downloader" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "yt_dlp-2024.8.6-py3-none-any.whl", hash = "sha256:ab507ff600bd9269ad4d654e309646976778f0e243eaa2f6c3c3214278bb2922"}, + {file = "yt_dlp-2024.8.6.tar.gz", hash = "sha256:e8551f26bc8bf67b99c12373cc87ed2073436c3437e53290878d0f4b4bb1f663"}, +] [package.dependencies] +brotli = {version = "*", markers = "implementation_name == \"cpython\""} +brotlicffi = {version = "*", markers = "implementation_name != \"cpython\""} +certifi = "*" mutagen = "*" pycryptodomex = "*" -websockets = "*" +requests = ">=2.32.2,<3" +urllib3 = ">=1.26.17,<3" +websockets = ">=12.0" -[metadata] -lock-version = "1.1" -python-versions = "~3.10.0" -content-hash = "1b135d1cedf49f0d2c7a1e974881285a0a377e10a836289d95357053b5250c15" +[package.extras] +build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"] +curl-cffi = ["curl-cffi (==0.5.10)", "curl-cffi (>=0.5.10,<0.6.dev0 || ==0.7.*)"] +dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "ruff (>=0.5.0,<0.6.0)"] +py2exe = ["py2exe (>=0.12)"] +pyinstaller = ["pyinstaller (>=6.7.0)"] +secretstorage = ["cffi", "secretstorage"] +static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.5.0,<0.6.0)"] +test = ["pytest (>=8.1,<9.0)"] -[metadata.files] -aiohttp = [ - {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, - {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, -] -async-timeout = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, -] -atomicwrites = [] -attrs = [] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, - {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, -] -bidict = [] -black = [] -certifi = [] -cffi = [] -chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] -charset-normalizer = [] -click = [] -colorama = [] -"discord.py" = [ - {file = "discord.py-1.6.0-py3-none-any.whl", hash = "sha256:3df148daf6fbcc7ab5b11042368a3cd5f7b730b62f09fb5d3cbceff59bcfbb12"}, - {file = "discord.py-1.6.0.tar.gz", hash = "sha256:ba8be99ff1b8c616f7b6dcb700460d0222b29d4c11048e74366954c465fdd05f"}, -] -feedparser = [] -googletrans = [ - {file = "googletrans-4.0.0rc1.tar.gz", hash = "sha256:74df47b092e2d566522019d149e3f1d75732570ad76eaf8e14aebeffc126c372"}, -] -h11 = [ - {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, - {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, -] -h2 = [ - {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, - {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, -] -hpack = [ - {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, - {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, -] -hstspreload = [] -httpcore = [ - {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, - {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"}, -] -httpx = [ - {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"}, - {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"}, -] -hyperframe = [ - {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, - {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -mpmath = [ - {file = "mpmath-1.2.1-py3-none-any.whl", hash = "sha256:604bc21bd22d2322a177c73bdb573994ef76e62edd595d17e00aff24b0667e5c"}, - {file = "mpmath-1.2.1.tar.gz", hash = "sha256:79ffb45cf9f4b101a807595bcb3e72e0396202e0b1d25d689134b48c4216a81a"}, -] -multidict = [ - {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, - {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, - {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, - {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, - {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, - {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, - {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, - {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, - {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, - {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, - {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, - {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, - {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, - {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, - {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, - {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, - {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, - {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, - {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, - {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, - {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, - {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, - {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, -] -mutagen = [ - {file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"}, - {file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -numpy = [] -opencv-python = [ - {file = "opencv-python-4.6.0.66.tar.gz", hash = "sha256:c5bfae41ad4031e66bb10ec4a0a2ffd3e514d092652781e8b1ac98d1b59f1158"}, - {file = "opencv_python-4.6.0.66-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:e6e448b62afc95c5b58f97e87ef84699e6607fe5c58730a03301c52496005cae"}, - {file = "opencv_python-4.6.0.66-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5af8ba35a4fcb8913ffb86e92403e9a656a4bff4a645d196987468f0f8947875"}, - {file = "opencv_python-4.6.0.66-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbdc84a9b4ea2cbae33861652d25093944b9959279200b7ae0badd32439f74de"}, - {file = "opencv_python-4.6.0.66-cp36-abi3-win32.whl", hash = "sha256:f482e78de6e7b0b060ff994ffd859bddc3f7f382bb2019ef157b0ea8ca8712f5"}, - {file = "opencv_python-4.6.0.66-cp36-abi3-win_amd64.whl", hash = "sha256:0dc82a3d8630c099d2f3ac1b1aabee164e8188db54a786abb7a4e27eba309440"}, - {file = "opencv_python-4.6.0.66-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:6e32af22e3202748bd233ed8f538741876191863882eba44e332d1a34993165b"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -pillow = [ - {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, - {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, - {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, - {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, - {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, - {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, - {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, - {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, - {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, - {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, - {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, - {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, - {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, - {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, - {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, - {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, - {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, - {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"}, - {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, -] -platformdirs = [] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pycryptodomex = [] -pynacl = [ - {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, - {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, - {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, - {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, - {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, - {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, - {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, - {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, - {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, - {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, - {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, - {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, - {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, -] -pyparsing = [] -pytest = [] -pytz = [] -regex = [ - {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, - {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"}, - {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, - {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, - {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"}, - {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, - {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, - {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"}, - {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, - {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"}, - {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, - {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"}, - {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, - {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, - {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, -] -requests = [] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -sgmllib3k = [ - {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -sniffio = [ - {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, - {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, -] -soupsieve = [] -sympy = [] -tabulate = [ - {file = "tabulate-0.8.9-py3-none-any.whl", hash = "sha256:d7c013fe7abbc5e491394e10fa845f8f32fe54f8dc60c6622c6cf482d25d47e4"}, - {file = "tabulate-0.8.9.tar.gz", hash = "sha256:eb1d13f25760052e8931f2ef80aaf6045a6cceb47514db8beab24cded16f13a7"}, -] -tomli = [] -typing-extensions = [] -urllib3 = [] -uvloop = [ - {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, - {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"}, - {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"}, - {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"}, - {file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"}, - {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"}, - {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"}, - {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"}, - {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"}, - {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"}, - {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"}, - {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"}, - {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"}, - {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"}, - {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"}, - {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, -] -websockets = [] -yarl = [] -yt-dlp = [ - {file = "yt-dlp-2021.12.27.tar.gz", hash = "sha256:2244df3759751487e796b23b67216bee98e70832a3a43c2526b0b0e0bbfbcb5b"}, - {file = "yt_dlp-2021.12.27-py2.py3-none-any.whl", hash = "sha256:bb46898a175d149c9c6bb2846056590d297aa4eafad8487c3e1315d2c6090896"}, -] +[metadata] +lock-version = "2.0" +python-versions = ">=3.10.0,<3.12" +content-hash = "74095f0b2be462abf5edf0397ec3c7349c9bd8c846d4afb4a7b93d59e4b4a5b7" diff --git a/pyproject.toml b/pyproject.toml index da1ff8a3c..8827235ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,36 +1,51 @@ [tool.poetry] name = "Canary" -version = "3.3.0" +version = "4.0.0" description = "" license = "GPL-3.0-only" repository = "https://github.com/idoneam/Canary" readme = "README.md" authors = [] +packages = [ + { include = "canary" }, +] + +[tool.poetry.scripts] +canary = "canary.main:main" [tool.poetry.dependencies] -python = "~3.10.0" -beautifulsoup4 = "==4.10.0" -sympy = "^1.9" -requests = ">=2.20.0" -tabulate = "==0.8.9" -"discord.py" = {extras = ["voice"], version = "~1.6.0"} -opencv-python = "==4.6.0.66" -pytz = ">=2020.5" -numpy = "^1.21.1" -aiohttp = "~3.7.4" -urllib3 = ">=1.25.3" +python = ">=3.10.0,<3.12" +beautifulsoup4 = "^4.12.3" +sympy = "^1.13.2" +requests = ">=2.32.3,<3" +tabulate = "~0.8.9" +"discord.py" = {version = "~1.7.3", extras = ["voice"]} +opencv-python = "==4.10.0.84" +pytz = ">=2024.1" +numpy = "^1.26.4" +aiohttp = "~3.7.4.post0" +urllib3 = "~2.0.7" feedparser = "^6.0.8" -regex = "^2021.11.10" +regex = "^2023.5.5" googletrans = "==4.0.0rc1" -yt-dlp = "^2021.12.27" -Pillow = "^8.3.2" -uvloop = {version="0.16.0", markers = "sys_platform == 'linux' or sys_platform == 'darwin'"} +yt-dlp = "^2024.4.9" +Pillow = "^9.5.0" +uvloop = {version="~0.20.0", markers = "sys_platform == 'linux' or sys_platform == 'darwin'"} bidict = "^0.22.0" +aiosqlite = "^0.19.0" +lxml = "^4.9.1" +python-dotenv = "^0.21.0" +pydantic = "^1.10.2" [tool.poetry.dev-dependencies] -pytest = "^7.0.1" -black = "^22.6" +pytest = "^7.3.1" +black = "^24.8.0" +mypy = "~1.11.2" +types-beautifulsoup4 = "^4.12.0.20240511" +types-regex = "^2024.7.24.20240726" +types-requests = "^2.32.0.20240712" +types-pytz = "^2024.1.0.20240417" +types-tabulate = "^0.8.11" [tool.black] line-length = 120 -