aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.dockerignore4
-rw-r--r--.github/CODEOWNERS3
-rw-r--r--.github/workflows/build.yaml4
-rw-r--r--.github/workflows/lint.yaml21
-rw-r--r--.github/workflows/sentry_release.yaml4
-rw-r--r--.github/workflows/status_embed.yaml4
-rw-r--r--.pre-commit-config.yaml4
-rw-r--r--Dockerfile33
-rw-r--r--Pipfile.lock811
-rw-r--r--bot/__init__.py11
-rw-r--r--bot/bot.py2
-rw-r--r--bot/constants.py25
-rw-r--r--bot/exts/christmas/advent_of_code/_cog.py25
-rw-r--r--bot/exts/christmas/advent_of_code/_helpers.py8
-rw-r--r--bot/exts/christmas/hanukkah_embed.py85
-rw-r--r--bot/exts/easter/april_fools_vids.py16
-rw-r--r--bot/exts/easter/bunny_name_generator.py42
-rw-r--r--bot/exts/easter/earth_photos.py11
-rw-r--r--bot/exts/easter/easter_riddle.py17
-rw-r--r--bot/exts/easter/egg_decorating.py29
-rw-r--r--bot/exts/easter/egg_facts.py22
-rw-r--r--bot/exts/easter/egghead_quiz.py31
-rw-r--r--bot/exts/easter/save_the_planet.py16
-rw-r--r--bot/exts/easter/traditions.py16
-rw-r--r--bot/exts/evergreen/avatar_modification/_effects.py12
-rw-r--r--bot/exts/evergreen/avatar_modification/avatar_modify.py18
-rw-r--r--bot/exts/evergreen/battleship.py35
-rw-r--r--bot/exts/evergreen/bookmark.py135
-rw-r--r--bot/exts/evergreen/catify.py10
-rw-r--r--bot/exts/evergreen/cheatsheet.py27
-rw-r--r--bot/exts/evergreen/connect_four.py95
-rw-r--r--bot/exts/evergreen/conversationstarters.py22
-rw-r--r--bot/exts/evergreen/emoji.py18
-rw-r--r--bot/exts/evergreen/error_handler.py21
-rw-r--r--bot/exts/evergreen/fun.py10
-rw-r--r--bot/exts/evergreen/game.py39
-rw-r--r--bot/exts/evergreen/githubinfo.py54
-rw-r--r--bot/exts/evergreen/help.py230
-rw-r--r--bot/exts/evergreen/issues.py27
-rw-r--r--bot/exts/evergreen/latex.py11
-rw-r--r--bot/exts/evergreen/magic_8ball.py17
-rw-r--r--bot/exts/evergreen/minesweeper.py57
-rw-r--r--bot/exts/evergreen/movie.py40
-rw-r--r--bot/exts/evergreen/ping.py5
-rw-r--r--bot/exts/evergreen/pythonfacts.py29
-rw-r--r--bot/exts/evergreen/recommend_game.py17
-rw-r--r--bot/exts/evergreen/snakes/__init__.py7
-rw-r--r--bot/exts/evergreen/snakes/_converter.py19
-rw-r--r--bot/exts/evergreen/snakes/_snakes_cog.py337
-rw-r--r--bot/exts/evergreen/snakes/_utils.py60
-rw-r--r--bot/exts/evergreen/source.py33
-rw-r--r--bot/exts/evergreen/space.py36
-rw-r--r--bot/exts/evergreen/speedrun.py13
-rw-r--r--bot/exts/evergreen/status_codes.py22
-rw-r--r--bot/exts/evergreen/tic_tac_toe.py9
-rw-r--r--bot/exts/evergreen/timed.py10
-rw-r--r--bot/exts/evergreen/trivia_quiz.py434
-rw-r--r--bot/exts/evergreen/wikipedia.py14
-rw-r--r--bot/exts/evergreen/wolfram.py45
-rw-r--r--bot/exts/evergreen/wonder_twins.py12
-rw-r--r--bot/exts/evergreen/xkcd.py6
-rw-r--r--bot/exts/halloween/8ball.py18
-rw-r--r--bot/exts/halloween/candy_collection.py59
-rw-r--r--bot/exts/halloween/hacktober-issue-finder.py77
-rw-r--r--bot/exts/halloween/hacktoberstats.py115
-rw-r--r--bot/exts/halloween/halloween_facts.py22
-rw-r--r--bot/exts/halloween/halloweenify.py30
-rw-r--r--bot/exts/halloween/monsterbio.py19
-rw-r--r--bot/exts/halloween/monstersurvey.py119
-rw-r--r--bot/exts/halloween/scarymovie.py92
-rw-r--r--bot/exts/halloween/spookygif.py26
-rw-r--r--bot/exts/halloween/spookynamerate.py50
-rw-r--r--bot/exts/halloween/spookyrating.py26
-rw-r--r--bot/exts/halloween/spookyreact.py43
-rw-r--r--bot/exts/halloween/timeleft.py11
-rw-r--r--bot/exts/internal_eval/_internal_eval.py10
-rw-r--r--bot/exts/pride/drag_queen_name.py24
-rw-r--r--bot/exts/pride/pride_anthem.py28
-rw-r--r--bot/exts/pride/pride_facts.py32
-rw-r--r--bot/exts/valentines/be_my_valentine.py70
-rw-r--r--bot/exts/valentines/lovecalculator.py28
-rw-r--r--bot/exts/valentines/movie_generator.py14
-rw-r--r--bot/exts/valentines/myvalenstate.py30
-rw-r--r--bot/exts/valentines/pickuplines.py23
-rw-r--r--bot/exts/valentines/savethedate.py17
-rw-r--r--bot/exts/valentines/valentine_zodiac.py52
-rw-r--r--bot/exts/valentines/whoisvalentine.py29
-rw-r--r--bot/resources/evergreen/trivia_quiz.json335
-rw-r--r--bot/utils/__init__.py43
-rw-r--r--bot/utils/checks.py30
-rw-r--r--bot/utils/converters.py83
-rw-r--r--bot/utils/decorators.py2
-rw-r--r--bot/utils/extensions.py4
-rw-r--r--bot/utils/halloween/spookifications.py10
-rw-r--r--bot/utils/pagination.py37
-rw-r--r--poetry.lock1337
-rw-r--r--pyproject.toml (renamed from Pipfile)28
97 files changed, 3743 insertions, 2460 deletions
diff --git a/.dockerignore b/.dockerignore
index 159e4f4c..cb6f8f88 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,6 +3,6 @@
# Make exceptions for what's needed
!bot
-!Pipfile
-!Pipfile.lock
+!pyproject.toml
+!poetry.lock
!LICENSE
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 16e89359..f41ef83b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -8,4 +8,5 @@ Dockerfile @Akarys42 @Den4200
docker-compose.yml @Akarys42 @Den4200
# Tools
-Pipfile* @Akarys42
+poetry.lock @Akarys42
+pyproject.toml @Akarys42
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index baa046ce..e857a6cf 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -8,6 +8,10 @@ on:
types:
- completed
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push'
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 7f157da3..87a4b530 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -6,6 +6,9 @@ on:
- main
pull_request:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
lint:
@@ -16,12 +19,8 @@ jobs:
PIP_NO_CACHE_DIR: false
PIP_USER: 1
- # Hide the graphical elements from pipenv's output
- PIPENV_HIDE_EMOJIS: 1
- PIPENV_NOSPIN: 1
-
- # Make sure pipenv does not try reuse an environment it's running in
- PIPENV_IGNORE_VIRTUALENVS: 1
+ # Disable Poetry virtualenv creation
+ POETRY_VIRTUALENVS_CREATE: false
# Specify explicit paths for python dependencies and the pre-commit
# environment so we know which directories to cache
@@ -39,7 +38,7 @@ jobs:
id: python
uses: actions/setup-python@v2
with:
- python-version: '3.8'
+ python-version: '3.9'
# This step caches our Python dependencies. To make sure we
# only restore a cache when the dependencies, the python version,
@@ -54,14 +53,14 @@ jobs:
path: ${{ env.PYTHONUSERBASE }}
key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\
${{ steps.python.outputs.python-version }}-\
- ${{ hashFiles('./Pipfile', './Pipfile.lock') }}"
+ ${{ hashFiles('./pyproject.toml', './poetry.lock') }}"
# Install our dependencies if we did not restore a dependency cache
- - name: Install dependencies using pipenv
+ - name: Install dependencies using poetry
if: steps.python_cache.outputs.cache-hit != 'true'
run: |
- pip install pipenv
- pipenv install --dev --deploy --system
+ pip install poetry
+ poetry install --no-interaction --no-ansi
# This step caches our pre-commit environment. To make sure we
# do create a new environment when our pre-commit setup changes,
diff --git a/.github/workflows/sentry_release.yaml b/.github/workflows/sentry_release.yaml
index 3d15e01e..c1073386 100644
--- a/.github/workflows/sentry_release.yaml
+++ b/.github/workflows/sentry_release.yaml
@@ -5,6 +5,10 @@ on:
branches:
- main
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
create_sentry_release:
runs-on: ubuntu-latest
diff --git a/.github/workflows/status_embed.yaml b/.github/workflows/status_embed.yaml
index 28caa8c2..737efe00 100644
--- a/.github/workflows/status_embed.yaml
+++ b/.github/workflows/status_embed.yaml
@@ -8,6 +8,10 @@ on:
types:
- completed
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
status_embed:
# We send the embed in the following situations:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6c94375c..7244cb4e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,8 +16,8 @@ repos:
hooks:
- id: flake8
name: Flake8
- description: This hook runs flake8 within our project's pipenv environment.
- entry: pipenv run flake8
+ description: This hook runs flake8 within our project's poetry environment.
+ entry: poetry run flake8
language: system
types: [python]
require_serial: true
diff --git a/Dockerfile b/Dockerfile
index 8c4920a9..2bad6450 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,32 +1,31 @@
-FROM python:3.8-slim
+FROM python:3.9-slim
# Set pip to have cleaner logs and no saved cache
ENV PIP_NO_CACHE_DIR=false \
- PIPENV_HIDE_EMOJIS=1 \
- PIPENV_IGNORE_VIRTUALENVS=1 \
- PIPENV_NOSPIN=1
+ POETRY_VIRTUALENVS_CREATE=false
-# Install pipenv
-RUN pip install -U pipenv
+# Install Poetry and add it to the path
+RUN pip install --user poetry
+ENV PATH="${PATH}:/root/.local/bin"
-# Copy the project files into working directory
WORKDIR /bot
-# Copy dependency files
-COPY Pipfile Pipfile.lock ./
+# Copy dependencies and lockfile
+COPY pyproject.toml poetry.lock /bot/
-# Install project dependencies
-RUN pipenv install --deploy --system
+# Install dependencies and lockfile, excluding development
+# dependencies,
+RUN poetry install --no-dev --no-interaction --no-ansi
-# Copy project code
-COPY . .
-
-# Set Git SHA enviroment variable
+# Set SHA build argument
ARG git_sha="development"
ENV GIT_SHA=$git_sha
-ENTRYPOINT ["python"]
-CMD ["-m", "bot"]
+# Copy the rest of the project code
+COPY . .
+
+# Start the bot
+CMD ["python", "-m", "bot"]
# Define docker persistent volumes
VOLUME /bot/bot/log /bot/data
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 915c3784..00000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,811 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "96cd9674aea76763df9582acd392eece6546876698fffaf9024e5a2daccb8f6f"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.8"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "aiodns": {
- "hashes": [
- "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d",
- "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de"
- ],
- "index": "pypi",
- "version": "==2.0.0"
- },
- "aiohttp": {
- "hashes": [
- "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe",
- "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599",
- "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8",
- "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a",
- "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37",
- "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5",
- "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0",
- "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c",
- "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645",
- "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98",
- "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d",
- "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81",
- "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5"
- ],
- "markers": "python_full_version >= '3.5.3'",
- "version": "==3.6.3"
- },
- "aioredis": {
- "hashes": [
- "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
- "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
- ],
- "version": "==1.3.1"
- },
- "arrow": {
- "hashes": [
- "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5",
- "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"
- ],
- "index": "pypi",
- "version": "==0.17.0"
- },
- "async-rediscache": {
- "extras": [
- "fakeredis"
- ],
- "hashes": [
- "sha256:6be8a657d724ccbcfb1946d29a80c3478c5f9ecd2f78a0a26d2f4013a622258f",
- "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af"
- ],
- "index": "pypi",
- "version": "==0.1.4"
- },
- "async-timeout": {
- "hashes": [
- "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
- "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
- ],
- "markers": "python_full_version >= '3.5.3'",
- "version": "==3.0.1"
- },
- "attrs": {
- "hashes": [
- "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
- "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.3.0"
- },
- "beautifulsoup4": {
- "hashes": [
- "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35",
- "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25",
- "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"
- ],
- "index": "pypi",
- "version": "==4.9.3"
- },
- "certifi": {
- "hashes": [
- "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
- "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
- ],
- "version": "==2020.12.5"
- },
- "cffi": {
- "hashes": [
- "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813",
- "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06",
- "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea",
- "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee",
- "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396",
- "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73",
- "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315",
- "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1",
- "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49",
- "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892",
- "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482",
- "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058",
- "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5",
- "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53",
- "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045",
- "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3",
- "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5",
- "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e",
- "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c",
- "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369",
- "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827",
- "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053",
- "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa",
- "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4",
- "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322",
- "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132",
- "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62",
- "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa",
- "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0",
- "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396",
- "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e",
- "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991",
- "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6",
- "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1",
- "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406",
- "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d",
- "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
- ],
- "version": "==1.14.5"
- },
- "chardet": {
- "hashes": [
- "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
- "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
- ],
- "version": "==3.0.4"
- },
- "cycler": {
- "hashes": [
- "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d",
- "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
- ],
- "version": "==0.10.0"
- },
- "discord.py": {
- "extras": [],
- "hashes": [
- "sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563",
- "sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b"
- ],
- "index": "pypi",
- "version": "==1.5.1"
- },
- "emojis": {
- "hashes": [
- "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c",
- "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec"
- ],
- "index": "pypi",
- "version": "==0.6.0"
- },
- "fakeredis": {
- "hashes": [
- "sha256:1ac0cef767c37f51718874a33afb5413e69d132988cb6a80c6e6dbeddf8c7623",
- "sha256:e0416e4941cecd3089b0d901e60c8dc3c944f6384f5e29e2261c0d3c5fa99669"
- ],
- "version": "==1.5.0"
- },
- "fuzzywuzzy": {
- "hashes": [
- "sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8",
- "sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993"
- ],
- "index": "pypi",
- "version": "==0.18.0"
- },
- "hiredis": {
- "hashes": [
- "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e",
- "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27",
- "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163",
- "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc",
- "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26",
- "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e",
- "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579",
- "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a",
- "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048",
- "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87",
- "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63",
- "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54",
- "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05",
- "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb",
- "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea",
- "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5",
- "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e",
- "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc",
- "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99",
- "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a",
- "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581",
- "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426",
- "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db",
- "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a",
- "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a",
- "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d",
- "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443",
- "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79",
- "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d",
- "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9",
- "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d",
- "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485",
- "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5",
- "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048",
- "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0",
- "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6",
- "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41",
- "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298",
- "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce",
- "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
- "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2.0.0"
- },
- "idna": {
- "hashes": [
- "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
- "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
- ],
- "markers": "python_version >= '3.4'",
- "version": "==3.1"
- },
- "kiwisolver": {
- "hashes": [
- "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d",
- "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31",
- "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9",
- "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0",
- "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72",
- "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3",
- "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6",
- "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e",
- "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000",
- "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3",
- "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18",
- "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21",
- "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621",
- "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b",
- "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc",
- "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131",
- "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882",
- "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454",
- "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248",
- "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de",
- "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598",
- "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54",
- "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278",
- "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6",
- "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81",
- "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030",
- "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8",
- "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689",
- "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4",
- "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0",
- "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05",
- "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==1.3.1"
- },
- "matplotlib": {
- "hashes": [
- "sha256:1f83a32e4b6045191f9d34e4dc68c0a17c870b57ef9cca518e516da591246e79",
- "sha256:2eee37340ca1b353e0a43a33da79d0cd4bcb087064a0c3c3d1329cdea8fbc6f3",
- "sha256:53ceb12ef44f8982b45adc7a0889a7e2df1d758e8b360f460e435abe8a8cd658",
- "sha256:574306171b84cd6854c83dc87bc353cacc0f60184149fb00c9ea871eca8c1ecb",
- "sha256:7561fd541477d41f3aa09457c434dd1f7604f3bd26d7858d52018f5dfe1c06d1",
- "sha256:7a54efd6fcad9cb3cd5ef2064b5a3eeb0b63c99f26c346bdcf66e7c98294d7cc",
- "sha256:7f16660edf9a8bcc0f766f51c9e1b9d2dc6ceff6bf636d2dbd8eb925d5832dfd",
- "sha256:81e6fe8b18ef5be67f40a1d4f07d5a4ed21d3878530193898449ddef7793952f",
- "sha256:84a10e462120aa7d9eb6186b50917ed5a6286ee61157bfc17c5b47987d1a9068",
- "sha256:84d4c4f650f356678a5d658a43ca21a41fca13f9b8b00169c0b76e6a6a948908",
- "sha256:86dc94e44403fa0f2b1dd76c9794d66a34e821361962fe7c4e078746362e3b14",
- "sha256:90dbc007f6389bcfd9ef4fe5d4c78c8d2efe4e0ebefd48b4f221cdfed5672be2",
- "sha256:9f374961a3996c2d1b41ba3145462c3708a89759e604112073ed6c8bdf9f622f",
- "sha256:a18cc1ab4a35b845cf33b7880c979f5c609fd26c2d6e74ddfacb73dcc60dd956",
- "sha256:a97781453ac79409ddf455fccf344860719d95142f9c334f2a8f3fff049ffec3",
- "sha256:a989022f89cda417f82dbf65e0a830832afd8af743d05d1414fb49549287ff04",
- "sha256:ac2a30a09984c2719f112a574b6543ccb82d020fd1b23b4d55bf4759ba8dd8f5",
- "sha256:be4430b33b25e127fc4ea239cc386389de420be4d63e71d5359c20b562951ce1",
- "sha256:c45e7bf89ea33a2adaef34774df4e692c7436a18a48bcb0e47a53e698a39fa39"
- ],
- "index": "pypi",
- "version": "==3.4.1"
- },
- "multidict": {
- "hashes": [
- "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a",
- "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000",
- "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2",
- "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507",
- "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5",
- "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7",
- "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d",
- "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463",
- "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19",
- "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3",
- "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b",
- "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c",
- "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87",
- "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7",
- "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430",
- "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255",
- "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==4.7.6"
- },
- "numpy": {
- "hashes": [
- "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727",
- "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6",
- "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98",
- "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7",
- "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d",
- "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2",
- "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9",
- "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935",
- "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff",
- "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee",
- "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb",
- "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042",
- "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3",
- "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5",
- "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6",
- "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f",
- "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4",
- "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737",
- "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931",
- "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6",
- "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677",
- "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576",
- "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935",
- "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.20.2"
- },
- "pillow": {
- "hashes": [
- "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5",
- "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4",
- "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9",
- "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a",
- "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9",
- "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727",
- "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120",
- "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c",
- "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2",
- "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797",
- "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b",
- "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f",
- "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef",
- "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232",
- "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb",
- "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9",
- "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812",
- "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178",
- "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b",
- "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5",
- "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b",
- "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1",
- "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713",
- "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4",
- "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484",
- "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c",
- "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9",
- "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388",
- "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d",
- "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602",
- "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9",
- "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e",
- "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"
- ],
- "index": "pypi",
- "version": "==8.2.0"
- },
- "pycares": {
- "hashes": [
- "sha256:050f00b39ed77ea8a4e555f09417d4b1a6b5baa24bb9531a3e15d003d2319b3f",
- "sha256:0a24d2e580a8eb567140d7b69f12cb7de90c836bd7b6488ec69394d308605ac3",
- "sha256:0c5bd1f6f885a219d5e972788d6eef7b8043b55c3375a845e5399638436e0bba",
- "sha256:11c628402cc8fc8ef461076d4e47f88afc1f8609989ebbff0dbffcd54c97239f",
- "sha256:18dfd4fd300f570d6c4536c1d987b7b7673b2a9d14346592c5d6ed716df0d104",
- "sha256:1917b82494907a4a342db420bc4dd5bac355a5fa3984c35ba9bf51422b020b48",
- "sha256:1b90fa00a89564df059fb18e796458864cc4e00cb55e364dbf921997266b7c55",
- "sha256:1d8d177c40567de78108a7835170f570ab04f09084bfd32df9919c0eaec47aa1",
- "sha256:236286f81664658b32c141c8e79d20afc3d54f6e2e49dfc8b702026be7265855",
- "sha256:2e4f74677542737fb5af4ea9a2e415ec5ab31aa67e7b8c3c969fdb15c069f679",
- "sha256:48a7750f04e69e1f304f4332b755728067e7c4b1abe2760bba1cacd9ff7a847a",
- "sha256:7d86e62b700b21401ffe7fd1bbfe91e08489416fecae99c6570ab023c6896022",
- "sha256:7e2d7effd08d2e5a3cb95d98a7286ebab71ab2fbce84fa93cc2dd56caf7240dd",
- "sha256:81edb016d9e43dde7473bc3999c29cdfee3a6b67308fed1ea21049f458e83ae0",
- "sha256:96c90e11b4a4c7c0b8ff5aaaae969c5035493136586043ff301979aae0623941",
- "sha256:9a0a1845f8cb2e62332bca0aaa9ad5494603ac43fb60d510a61d5b5b170d7216",
- "sha256:a05bbfdfd41f8410a905a818f329afe7510cbd9ee65c60f8860a72b6c64ce5dc",
- "sha256:a5089fd660f0b0d228b14cdaa110d0d311edfa5a63f800618dbf1321dcaef66b",
- "sha256:c457a709e6f2befea7e2996c991eda6d79705dd075f6521593ba6ebc1485b811",
- "sha256:c5cb72644b04e5e5abfb1e10a0e7eb75da6684ea0e60871652f348e412cf3b11",
- "sha256:cce46dd4717debfd2aab79d6d7f0cbdf6b1e982dc4d9bebad81658d59ede07c2",
- "sha256:cfdd1f90bcf373b00f4b2c55ea47868616fe2f779f792fc913fa82a3d64ffe43",
- "sha256:d88a279cbc5af613f73e86e19b3f63850f7a2e2736e249c51995dedcc830b1bb",
- "sha256:eba9a9227438da5e78fc8eee32f32eb35d9a50cf0a0bd937eb6275c7cc3015fe",
- "sha256:eee7b6a5f5b5af050cb7d66ab28179287b416f06d15a8974ac831437fec51336",
- "sha256:f41ac1c858687e53242828c9f59c2e7b0b95dbcd5bdd09c7e5d3c48b0f89a25a",
- "sha256:f8deaefefc3a589058df1b177275f79233e8b0eeee6734cf4336d80164ecd022",
- "sha256:fa78e919f3bd7d6d075db262aa41079b4c02da315c6043c6f43881e2ebcdd623",
- "sha256:fadb97d2e02dabdc15a0091591a972a938850d79ddde23d385d813c1731983f0"
- ],
- "version": "==3.1.1"
- },
- "pycparser": {
- "hashes": [
- "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
- "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.20"
- },
- "pyparsing": {
- "hashes": [
- "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
- "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
- ],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.4.7"
- },
- "python-dateutil": {
- "hashes": [
- "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
- "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.8.1"
- },
- "pytz": {
- "hashes": [
- "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
- "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
- ],
- "index": "pypi",
- "version": "==2019.3"
- },
- "pyyaml": {
- "hashes": [
- "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
- "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
- "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
- "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
- "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
- "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
- "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
- "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
- "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
- "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
- "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
- "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
- "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
- "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
- "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
- "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
- "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
- "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
- "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
- "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
- "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
- "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
- "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
- "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
- "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
- "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
- "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
- "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
- "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
- ],
- "index": "pypi",
- "version": "==5.4.1"
- },
- "redis": {
- "hashes": [
- "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
- "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==3.5.3"
- },
- "sentry-sdk": {
- "hashes": [
- "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237",
- "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"
- ],
- "index": "pypi",
- "version": "==0.20.3"
- },
- "six": {
- "hashes": [
- "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
- "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.15.0"
- },
- "sortedcontainers": {
- "hashes": [
- "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f",
- "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"
- ],
- "version": "==2.3.0"
- },
- "soupsieve": {
- "hashes": [
- "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc",
- "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"
- ],
- "markers": "python_version >= '3.0'",
- "version": "==2.2.1"
- },
- "urllib3": {
- "hashes": [
- "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
- "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.26.4"
- },
- "yarl": {
- "hashes": [
- "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409",
- "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593",
- "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2",
- "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8",
- "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d",
- "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692",
- "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02",
- "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a",
- "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8",
- "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6",
- "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511",
- "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e",
- "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a",
- "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb",
- "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f",
- "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317",
- "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.5.1"
- }
- },
- "develop": {
- "appdirs": {
- "hashes": [
- "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
- "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
- ],
- "version": "==1.4.4"
- },
- "attrs": {
- "hashes": [
- "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
- "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.3.0"
- },
- "cfgv": {
- "hashes": [
- "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d",
- "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"
- ],
- "markers": "python_full_version >= '3.6.1'",
- "version": "==3.2.0"
- },
- "distlib": {
- "hashes": [
- "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb",
- "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"
- ],
- "version": "==0.3.1"
- },
- "filelock": {
- "hashes": [
- "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
- "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
- ],
- "version": "==3.0.12"
- },
- "flake8": {
- "hashes": [
- "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378",
- "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"
- ],
- "index": "pypi",
- "version": "==3.9.1"
- },
- "flake8-annotations": {
- "hashes": [
- "sha256:0d6cd2e770b5095f09689c9d84cc054c51b929c41a68969ea1beb4b825cac515",
- "sha256:d10c4638231f8a50c0a597c4efce42bd7b7d85df4f620a0ddaca526138936a4f"
- ],
- "index": "pypi",
- "version": "==2.6.2"
- },
- "flake8-bugbear": {
- "hashes": [
- "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538",
- "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"
- ],
- "index": "pypi",
- "version": "==20.11.1"
- },
- "flake8-docstrings": {
- "hashes": [
- "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde",
- "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"
- ],
- "index": "pypi",
- "version": "==1.6.0"
- },
- "flake8-import-order": {
- "hashes": [
- "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543",
- "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"
- ],
- "index": "pypi",
- "version": "==0.18.1"
- },
- "flake8-polyfill": {
- "hashes": [
- "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9",
- "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"
- ],
- "version": "==1.0.2"
- },
- "flake8-string-format": {
- "hashes": [
- "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2",
- "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"
- ],
- "index": "pypi",
- "version": "==0.3.0"
- },
- "flake8-tidy-imports": {
- "hashes": [
- "sha256:52e5f2f987d3d5597538d5941153409ebcab571635835b78f522c7bf03ca23bc",
- "sha256:76e36fbbfdc8e3c5017f9a216c2855a298be85bc0631e66777f4e6a07a859dc4"
- ],
- "index": "pypi",
- "version": "==4.2.1"
- },
- "flake8-todo": {
- "hashes": [
- "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"
- ],
- "index": "pypi",
- "version": "==0.7"
- },
- "identify": {
- "hashes": [
- "sha256:398cb92a7599da0b433c65301a1b62b9b1f4bb8248719b84736af6c0b22289d6",
- "sha256:4537474817e0bbb8cea3e5b7504b7de6d44e3f169a90846cbc6adb0fc8294502"
- ],
- "markers": "python_full_version >= '3.6.1'",
- "version": "==2.2.3"
- },
- "mccabe": {
- "hashes": [
- "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
- "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
- ],
- "version": "==0.6.1"
- },
- "nodeenv": {
- "hashes": [
- "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b",
- "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"
- ],
- "version": "==1.6.0"
- },
- "pep8-naming": {
- "hashes": [
- "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724",
- "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"
- ],
- "index": "pypi",
- "version": "==0.11.1"
- },
- "pre-commit": {
- "hashes": [
- "sha256:029d53cb83c241fe7d66eeee1e24db426f42c858f15a38d20bcefd8d8e05c9da",
- "sha256:46b6ffbab37986c47d0a35e40906ae029376deed89a0eb2e446fb6e67b220427"
- ],
- "index": "pypi",
- "version": "==2.12.0"
- },
- "pycodestyle": {
- "hashes": [
- "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
- "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.7.0"
- },
- "pydocstyle": {
- "hashes": [
- "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f",
- "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==6.0.0"
- },
- "pyflakes": {
- "hashes": [
- "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
- "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.3.1"
- },
- "pyyaml": {
- "hashes": [
- "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
- "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
- "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
- "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
- "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
- "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
- "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
- "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
- "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
- "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
- "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
- "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
- "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
- "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
- "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
- "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
- "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
- "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
- "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
- "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
- "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
- "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
- "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
- "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
- "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
- "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
- "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
- "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
- "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
- ],
- "index": "pypi",
- "version": "==5.4.1"
- },
- "six": {
- "hashes": [
- "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
- "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.15.0"
- },
- "snowballstemmer": {
- "hashes": [
- "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
- "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"
- ],
- "version": "==2.1.0"
- },
- "toml": {
- "hashes": [
- "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
- "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
- ],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==0.10.2"
- },
- "virtualenv": {
- "hashes": [
- "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107",
- "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.4.3"
- }
- }
-}
diff --git a/bot/__init__.py b/bot/__init__.py
index 71b7c8a3..85ae4758 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,3 +1,10 @@
+try:
+ from dotenv import load_dotenv
+ print("Found .env file, loading environment variables from it.")
+ load_dotenv(override=True)
+except ModuleNotFoundError:
+ pass
+
import asyncio
import logging
import logging.handlers
@@ -64,12 +71,12 @@ logging.getLogger("matplotlib").setLevel(logging.ERROR)
# Setup new logging configuration
logging.basicConfig(
- format='%(asctime)s - %(name)s %(levelname)s: %(message)s',
+ format="%(asctime)s - %(name)s %(levelname)s: %(message)s",
datefmt="%D %H:%M:%S",
level=logging.TRACE if Client.debug else logging.DEBUG,
handlers=[console_handler, file_handler],
)
-logging.getLogger().info('Logging initialization complete')
+logging.getLogger().info("Logging initialization complete")
# On Windows, the selector event loop is required for aiodns.
diff --git a/bot/bot.py b/bot/bot.py
index 7e495940..b8de97aa 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -101,7 +101,7 @@ class Bot(commands.Bot):
all_channels_ids = [channel.id for channel in self.get_all_channels()]
for name, channel_id in vars(constants.Channels).items():
- if name.startswith('_'):
+ if name.startswith("_"):
continue
if channel_id not in all_channels_ids:
log.error(f'Channel "{name}" with ID {channel_id} missing')
diff --git a/bot/constants.py b/bot/constants.py
index 549d01b6..28ec477d 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -140,19 +140,20 @@ class Client(NamedTuple):
class Colours:
- blue = 0x0279fd
- bright_green = 0x01d277
- dark_green = 0x1f8b4c
- orange = 0xe67e22
- pink = 0xcf84e0
- purple = 0xb734eb
- soft_green = 0x68c290
- soft_orange = 0xf9cb54
- soft_red = 0xcd6d6d
- yellow = 0xf9f586
+ blue = 0x0279FD
+ bright_green = 0x01D277
+ dark_green = 0x1F8B4C
+ orange = 0xE67E22
+ pink = 0xCF84E0
+ purple = 0xB734EB
+ soft_green = 0x68C290
+ soft_orange = 0xF9CB54
+ soft_red = 0xCD6D6D
+ yellow = 0xF9F586
python_blue = 0x4B8BBE
python_yellow = 0xFFD43B
- grass_green = 0x66ff00
+ grass_green = 0x66FF00
+ gold = 0xE6C200
easter_like_colours = [
(255, 247, 0),
@@ -179,7 +180,7 @@ class Emojis:
envelope = "\U0001F4E8"
trashcan = environ.get("TRASHCAN_EMOJI", "<:trashcan:637136429717389331>")
ok_hand = ":ok_hand:"
- hand_raised = "\U0001f64b"
+ hand_raised = "\U0001F64B"
dice_1 = "<:dice_1:755891608859443290>"
dice_2 = "<:dice_2:755891608741740635>"
diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py
index 8376987d..ead84544 100644
--- a/bot/exts/christmas/advent_of_code/_cog.py
+++ b/bot/exts/christmas/advent_of_code/_cog.py
@@ -72,11 +72,15 @@ class AdventOfCode(commands.Cog):
if role not in ctx.author.roles:
await ctx.author.add_roles(role)
- await ctx.send("Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. "
- f"You can run `{unsubscribe_command}` to disable them again for you.")
+ await ctx.send(
+ "Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. "
+ f"You can run `{unsubscribe_command}` to disable them again for you."
+ )
else:
- await ctx.send("Hey, you already are receiving notifications about new Advent of Code tasks. "
- f"If you don't want them any more, run `{unsubscribe_command}` instead.")
+ await ctx.send(
+ "Hey, you already are receiving notifications about new Advent of Code tasks. "
+ f"If you don't want them any more, run `{unsubscribe_command}` instead."
+ )
@in_month(Month.DECEMBER)
@adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
@@ -110,8 +114,10 @@ class AdventOfCode(commands.Cog):
else:
delta_str = f"{delta.days} days"
- await ctx.send(f"The Advent of Code event is not currently running. "
- f"The next event will start in {delta_str}.")
+ await ctx.send(
+ "The Advent of Code event is not currently running. "
+ f"The next event will start in {delta_str}."
+ )
return
tomorrow, time_left = _helpers.time_left_to_est_midnight()
@@ -124,7 +130,7 @@ class AdventOfCode(commands.Cog):
@whitelist_override(channels=AOC_WHITELIST)
async def about_aoc(self, ctx: commands.Context) -> None:
"""Respond with an explanation of all things Advent of Code."""
- await ctx.send("", embed=self.cached_about_aoc)
+ await ctx.send(embed=self.cached_about_aoc)
@adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)")
@whitelist_override(channels=AOC_WHITELIST)
@@ -135,7 +141,7 @@ class AdventOfCode(commands.Cog):
await ctx.send(f"The Python Discord leaderboard for {current_year} is not yet available!")
return
- author = ctx.message.author
+ author = ctx.author
log.info(f"{author.name} ({author.id}) has requested a PyDis AoC leaderboard code")
if AocConfig.staff_leaderboard_id and any(r.id == Roles.helpers for r in author.roles):
@@ -273,8 +279,7 @@ class AdventOfCode(commands.Cog):
def _build_about_embed(self) -> discord.Embed:
"""Build and return the informational "About AoC" embed from the resources file."""
- with self.about_aoc_filepath.open("r", encoding="utf8") as f:
- embed_fields = json.load(f)
+ embed_fields = json.loads(self.about_aoc_filepath.read_text("utf8"))
about_embed = discord.Embed(
title=self._base_url,
diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py
index a16a4871..f4a258c0 100644
--- a/bot/exts/christmas/advent_of_code/_helpers.py
+++ b/bot/exts/christmas/advent_of_code/_helpers.py
@@ -108,7 +108,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:
# star view. We need that per star view to compute rank scores per star.
for member in raw_leaderboard_data.values():
name = member["name"] if member["name"] else f"Anonymous #{member['id']}"
- member_id = member['id']
+ member_id = member["id"]
leaderboard[member_id] = {"name": name, "score": 0, "star_1": 0, "star_2": 0}
# Iterate over all days for this participant
@@ -119,7 +119,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:
leaderboard[member_id][f"star_{star}"] += 1
# Record completion datetime for this participant for this day/star
- completion_time = datetime.datetime.fromtimestamp(int(data['get_star_ts']))
+ completion_time = datetime.datetime.fromtimestamp(int(data["get_star_ts"]))
star_results[(day, star)].append(
StarResult(member_id=member_id, completion_time=completion_time)
)
@@ -133,7 +133,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:
if day in AdventOfCode.ignored_days:
continue
- sorted_result = sorted(results, key=operator.attrgetter('completion_time'))
+ sorted_result = sorted(results, key=operator.attrgetter("completion_time"))
for rank, star_result in enumerate(sorted_result):
leaderboard[star_result.member_id]["score"] += max_score - rank
@@ -307,7 +307,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict:
def get_summary_embed(leaderboard: dict) -> discord.Embed:
"""Get an embed with the current summary stats of the leaderboard."""
- leaderboard_url = leaderboard['full_leaderboard_url']
+ leaderboard_url = leaderboard["full_leaderboard_url"]
refresh_minutes = AdventOfCode.leaderboard_cache_expiry_seconds // 60
aoc_embed = discord.Embed(
diff --git a/bot/exts/christmas/hanukkah_embed.py b/bot/exts/christmas/hanukkah_embed.py
index 4f470a34..119f2446 100644
--- a/bot/exts/christmas/hanukkah_embed.py
+++ b/bot/exts/christmas/hanukkah_embed.py
@@ -5,19 +5,23 @@ from typing import List
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours, Month
from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
+HEBCAL_URL = (
+ "https://www.hebcal.com/hebcal/?v=1&cfg=json&maj=on&min=on&mod=on&nx=on&"
+ "year=now&month=x&ss=on&mf=on&c=on&geo=geoname&geonameid=3448439&m=50&s=on"
+)
+
class HanukkahEmbed(commands.Cog):
"""A cog that returns information about Hanukkah festival."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- self.url = ("https://www.hebcal.com/hebcal/?v=1&cfg=json&maj=on&min=on&mod=on&nx=on&"
- "year=now&month=x&ss=on&mf=on&c=on&geo=geoname&geonameid=3448439&m=50&s=on")
self.hanukkah_days = []
self.hanukkah_months = []
self.hanukkah_years = []
@@ -25,17 +29,17 @@ class HanukkahEmbed(commands.Cog):
async def get_hanukkah_dates(self) -> List[str]:
"""Gets the dates for hanukkah festival."""
hanukkah_dates = []
- async with self.bot.http_session.get(self.url) as response:
+ async with self.bot.http_session.get(HEBCAL_URL) as response:
json_data = await response.json()
- festivals = json_data['items']
+ festivals = json_data["items"]
for festival in festivals:
- if festival['title'].startswith('Chanukah'):
- date = festival['date']
+ if festival["title"].startswith("Chanukah"):
+ date = festival["date"]
hanukkah_dates.append(date)
return hanukkah_dates
@in_month(Month.DECEMBER)
- @commands.command(name='hanukkah', aliases=['chanukah'])
+ @commands.command(name="hanukkah", aliases=("chanukah",))
async def hanukkah_festival(self, ctx: commands.Context) -> None:
"""Tells you about the Hanukkah Festivaltime of festival, festival day, etc)."""
hanukkah_dates = await self.get_hanukkah_dates()
@@ -54,49 +58,46 @@ class HanukkahEmbed(commands.Cog):
day = str(today.day)
month = str(today.month)
year = str(today.year)
- embed = Embed()
- embed.title = 'Hanukkah'
- embed.colour = Colours.blue
+ embed = Embed(title="Hanukkah", colour=Colours.blue)
if day in self.hanukkah_days and month in self.hanukkah_months and year in self.hanukkah_years:
if int(day) == hanukkah_start_day:
now = datetime.datetime.utcnow()
- now = str(now)
- hours = int(now[11:13]) + 4 # using only hours
+ hours = now.hour + 4 # using only hours
hanukkah_start_hour = 18
if hours < hanukkah_start_hour:
- embed.description = (f"Hanukkah hasnt started yet, "
- f"it will start in about {hanukkah_start_hour-hours} hour/s.")
- return await ctx.send(embed=embed)
+ embed.description = (
+ "Hanukkah hasnt started yet, "
+ f"it will start in about {hanukkah_start_hour - hours} hour/s."
+ )
+ await ctx.send(embed=embed)
+ return
elif hours > hanukkah_start_hour:
- embed.description = (f'It is the starting day of Hanukkah ! '
- f'Its been {hours-hanukkah_start_hour} hours hanukkah started !')
- return await ctx.send(embed=embed)
+ embed.description = (
+ "It is the starting day of Hanukkah! "
+ f"Its been {hours - hanukkah_start_hour} hours hanukkah started!"
+ )
+ await ctx.send(embed=embed)
+ return
festival_day = self.hanukkah_days.index(day)
- number_suffixes = ['st', 'nd', 'rd', 'th']
- suffix = ''
- if int(festival_day) == 1:
- suffix = number_suffixes[0]
- if int(festival_day) == 2:
- suffix = number_suffixes[1]
- if int(festival_day) == 3:
- suffix = number_suffixes[2]
- if int(festival_day) > 3:
- suffix = number_suffixes[3]
- message = ''
- for _ in range(1, festival_day + 1):
- message += ':menorah:'
- embed.description = f'It is the {festival_day}{suffix} day of Hanukkah ! \n {message}'
+ number_suffixes = ["st", "nd", "rd", "th"]
+ suffix = number_suffixes[festival_day - 1 if festival_day <= 3 else 3]
+ message = ":menorah:" * festival_day
+ embed.description = f"It is the {festival_day}{suffix} day of Hanukkah!\n{message}"
await ctx.send(embed=embed)
else:
if today < hanukkah_start:
- festival_starting_month = hanukkah_start.strftime('%B')
- embed.description = (f"Hanukkah has not started yet. "
- f"Hanukkah will start at sundown on {hanukkah_start_day}th "
- f"of {festival_starting_month}.")
+ festival_starting_month = hanukkah_start.strftime("%B")
+ embed.description = (
+ f"Hanukkah has not started yet. "
+ f"Hanukkah will start at sundown on {hanukkah_start_day}th "
+ f"of {festival_starting_month}."
+ )
else:
- festival_end_month = hanukkah_end.strftime('%B')
- embed.description = (f"Looks like you missed Hanukkah !"
- f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}.")
+ festival_end_month = hanukkah_end.strftime("%B")
+ embed.description = (
+ f"Looks like you missed Hanukkah!"
+ f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}."
+ )
await ctx.send(embed=embed)
@@ -108,6 +109,6 @@ class HanukkahEmbed(commands.Cog):
self.hanukkah_years.append(date[0:4])
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Hanukkah Embed Cog."""
bot.add_cog(HanukkahEmbed(bot))
diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py
index c7a3c014..5ef40704 100644
--- a/bot/exts/easter/april_fools_vids.py
+++ b/bot/exts/easter/april_fools_vids.py
@@ -1,19 +1,21 @@
import logging
import random
-from json import load
+from json import loads
+from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with open("bot/resources/easter/april_fools_vids.json", encoding="utf-8") as f:
- ALL_VIDS = load(f)
+ALL_VIDS = loads(Path("bot/resources/easter/april_fools_vids.json").read_text("utf-8"))
class AprilFoolVideos(commands.Cog):
"""A cog for April Fools' that gets a random April Fools' video from Youtube."""
- @commands.command(name='fool')
+ @commands.command(name="fool")
async def april_fools(self, ctx: commands.Context) -> None:
"""Get a random April Fools' video from Youtube."""
video = random.choice(ALL_VIDS)
@@ -23,6 +25,6 @@ class AprilFoolVideos(commands.Cog):
await ctx.send(f"Check out this April Fools' video by {channel}.\n\n{url}")
-def setup(bot: commands.Bot) -> None:
- """April Fools' Cog load."""
- bot.add_cog(AprilFoolVideos(bot))
+def setup(bot: Bot) -> None:
+ """Load the April Fools' Cog."""
+ bot.add_cog(AprilFoolVideos())
diff --git a/bot/exts/easter/bunny_name_generator.py b/bot/exts/easter/bunny_name_generator.py
index 3ecf9be9..3e97373f 100644
--- a/bot/exts/easter/bunny_name_generator.py
+++ b/bot/exts/easter/bunny_name_generator.py
@@ -7,25 +7,26 @@ from typing import List, Union
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with Path("bot/resources/easter/bunny_names.json").open("r", encoding="utf8") as f:
- BUNNY_NAMES = json.load(f)
+BUNNY_NAMES = json.loads(Path("bot/resources/easter/bunny_names.json").read_text("utf8"))
class BunnyNameGenerator(commands.Cog):
"""Generate a random bunny name, or bunnify your Discord username!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- def find_separators(self, displayname: str) -> Union[List[str], None]:
+ @staticmethod
+ def find_separators(displayname: str) -> Union[List[str], None]:
"""Check if Discord name contains spaces so we can bunnify an individual word in the name."""
- new_name = re.split(r'[_.\s]', displayname)
+ new_name = re.split(r"[_.\s]", displayname)
if displayname not in new_name:
return new_name
+ return None
- def find_vowels(self, displayname: str) -> str:
+ @staticmethod
+ def find_vowels(displayname: str) -> str:
"""
Finds vowels in the user's display name.
@@ -34,11 +35,11 @@ class BunnyNameGenerator(commands.Cog):
Only the most recently matched pattern will apply the changes.
"""
expressions = [
- (r'a.+y', 'patchy'),
- (r'e.+y', 'ears'),
- (r'i.+y', 'ditsy'),
- (r'o.+y', 'oofy'),
- (r'u.+y', 'uffy'),
+ ("a.+y", "patchy"),
+ ("e.+y", "ears"),
+ ("i.+y", "ditsy"),
+ ("o.+y", "oofy"),
+ ("u.+y", "uffy"),
]
for exp, vowel_sub in expressions:
@@ -46,9 +47,10 @@ class BunnyNameGenerator(commands.Cog):
if new_name != displayname:
return new_name
- def append_name(self, displayname: str) -> str:
+ @staticmethod
+ def append_name(displayname: str) -> str:
"""Adds a suffix to the end of the Discord name."""
- extensions = ['foot', 'ear', 'nose', 'tail']
+ extensions = ["foot", "ear", "nose", "tail"]
suffix = random.choice(extensions)
appended_name = displayname + suffix
@@ -62,7 +64,7 @@ class BunnyNameGenerator(commands.Cog):
@commands.command()
async def bunnifyme(self, ctx: commands.Context) -> None:
"""Gets your Discord username and bunnifies it."""
- username = ctx.message.author.display_name
+ username = ctx.author.display_name
# If name contains spaces or other separators, get the individual words to randomly bunnify
spaces_in_name = self.find_separators(username)
@@ -75,7 +77,7 @@ class BunnyNameGenerator(commands.Cog):
unmatched_name = self.append_name(username)
if spaces_in_name is not None:
- replacements = ['Cotton', 'Fluff', 'Floof' 'Bounce', 'Snuffle', 'Nibble', 'Cuddle', 'Velvetpaw', 'Carrot']
+ replacements = ["Cotton", "Fluff", "Floof" "Bounce", "Snuffle", "Nibble", "Cuddle", "Velvetpaw", "Carrot"]
word_to_replace = random.choice(spaces_in_name)
substitute = random.choice(replacements)
bunnified_name = username.replace(word_to_replace, substitute)
@@ -87,6 +89,6 @@ class BunnyNameGenerator(commands.Cog):
await ctx.send(bunnified_name)
-def setup(bot: commands.Bot) -> None:
- """Bunny Name Generator Cog load."""
- bot.add_cog(BunnyNameGenerator(bot))
+def setup(bot: Bot) -> None:
+ """Load the Bunny Name Generator Cog."""
+ bot.add_cog(BunnyNameGenerator())
diff --git a/bot/exts/easter/earth_photos.py b/bot/exts/easter/earth_photos.py
index bf658391..f65790af 100644
--- a/bot/exts/easter/earth_photos.py
+++ b/bot/exts/easter/earth_photos.py
@@ -3,24 +3,27 @@ import logging
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
from bot.constants import Tokens
log = logging.getLogger(__name__)
+API_URL = "https://api.unsplash.com/photos/random"
+
class EarthPhotos(commands.Cog):
"""This cog contains the command for earth photos."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- @commands.command(aliases=["earth"])
+ @commands.command(aliases=("earth",))
async def earth_photos(self, ctx: commands.Context) -> None:
"""Returns a random photo of earth, sourced from Unsplash."""
async with ctx.typing():
async with self.bot.http_session.get(
- 'https://api.unsplash.com/photos/random',
+ API_URL,
params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key}
) as r:
jsondata = await r.json()
@@ -55,7 +58,7 @@ class EarthPhotos(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Earth Photos cog."""
if not Tokens.unsplash_access_key:
log.warning("No Unsplash access key found. Cog not loading.")
diff --git a/bot/exts/easter/easter_riddle.py b/bot/exts/easter/easter_riddle.py
index a93b3745..88b3be2f 100644
--- a/bot/exts/easter/easter_riddle.py
+++ b/bot/exts/easter/easter_riddle.py
@@ -1,18 +1,18 @@
import asyncio
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours, NEGATIVE_REPLIES
log = logging.getLogger(__name__)
-with Path("bot/resources/easter/easter_riddle.json").open("r", encoding="utf8") as f:
- RIDDLE_QUESTIONS = load(f)
+RIDDLE_QUESTIONS = loads(Path("bot/resources/easter/easter_riddle.json").read_text("utf8"))
TIMELIMIT = 10
@@ -20,13 +20,13 @@ TIMELIMIT = 10
class EasterRiddle(commands.Cog):
"""This cog contains the command for the Easter quiz!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.winners = set()
self.correct = ""
self.current_channel = None
- @commands.command(aliases=["riddlemethis", "riddleme"])
+ @commands.command(aliases=("riddlemethis", "riddleme"))
async def riddle(self, ctx: commands.Context) -> None:
"""
Gives a random riddle, then provides 2 hints at certain intervals before revealing the answer.
@@ -34,7 +34,8 @@ class EasterRiddle(commands.Cog):
The duration of the hint interval can be configured by changing the TIMELIMIT constant in this file.
"""
if self.current_channel:
- return await ctx.send(f"A riddle is already being solved in {self.current_channel.mention}!")
+ await ctx.send(f"A riddle is already being solved in {self.current_channel.mention}!")
+ return
# Don't let users start in a DM
if not ctx.guild:
@@ -47,7 +48,7 @@ class EasterRiddle(commands.Cog):
)
return
- self.current_channel = ctx.message.channel
+ self.current_channel = ctx.channel
random_question = random.choice(RIDDLE_QUESTIONS)
question = random_question["question"]
@@ -106,6 +107,6 @@ class EasterRiddle(commands.Cog):
self.winners.add(message.author.mention)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Easter Riddle Cog load."""
bot.add_cog(EasterRiddle(bot))
diff --git a/bot/exts/easter/egg_decorating.py b/bot/exts/easter/egg_decorating.py
index a847388d..fd7620d4 100644
--- a/bot/exts/easter/egg_decorating.py
+++ b/bot/exts/easter/egg_decorating.py
@@ -10,15 +10,14 @@ import discord
from PIL import Image
from discord.ext import commands
+from bot.bot import Bot
from bot.utils import helpers
log = logging.getLogger(__name__)
-with open(Path("bot/resources/evergreen/html_colours.json"), encoding="utf8") as f:
- HTML_COLOURS = json.load(f)
+HTML_COLOURS = json.loads(Path("bot/resources/evergreen/html_colours.json").read_text("utf8"))
-with open(Path("bot/resources/evergreen/xkcd_colours.json"), encoding="utf8") as f:
- XKCD_COLOURS = json.load(f)
+XKCD_COLOURS = json.loads(Path("bot/resources/evergreen/xkcd_colours.json").read_text("utf8"))
COLOURS = [
(255, 0, 0, 255), (255, 128, 0, 255), (255, 255, 0, 255), (0, 255, 0, 255),
@@ -33,9 +32,6 @@ IRREPLACEABLE = [
class EggDecorating(commands.Cog):
"""Decorate some easter eggs!"""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
@staticmethod
def replace_invalid(colour: str) -> Union[int, None]:
"""Attempts to match with HTML or XKCD colour names, returning the int value."""
@@ -45,10 +41,10 @@ class EggDecorating(commands.Cog):
return int(XKCD_COLOURS[colour], 16)
return None
- @commands.command(aliases=["decorateegg"])
+ @commands.command(aliases=("decorateegg",))
async def eggdecorate(
self, ctx: commands.Context, *colours: Union[discord.Colour, str]
- ) -> Union[Image.Image, discord.Message]:
+ ) -> Union[Image.Image, None]:
"""
Picks a random egg design and decorates it using the given colours.
@@ -56,7 +52,8 @@ class EggDecorating(commands.Cog):
Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
"""
if len(colours) < 2:
- return await ctx.send("You must include at least 2 colours!")
+ await ctx.send("You must include at least 2 colours!")
+ return
invalid = []
colours = list(colours)
@@ -70,9 +67,11 @@ class EggDecorating(commands.Cog):
invalid.append(helpers.suppress_links(colour))
if len(invalid) > 1:
- return await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
+ await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
+ return
elif len(invalid) == 1:
- return await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
+ await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
+ return
async with ctx.typing():
# Expand list to 8 colours
@@ -115,6 +114,6 @@ class EggDecorating(commands.Cog):
return new_im
-def setup(bot: commands.bot) -> None:
- """Egg decorating Cog load."""
- bot.add_cog(EggDecorating(bot))
+def setup(bot: Bot) -> None:
+ """Load the Egg decorating Cog."""
+ bot.add_cog(EggDecorating())
diff --git a/bot/exts/easter/egg_facts.py b/bot/exts/easter/egg_facts.py
index 761e9059..486e735f 100644
--- a/bot/exts/easter/egg_facts.py
+++ b/bot/exts/easter/egg_facts.py
@@ -1,6 +1,6 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
@@ -12,6 +12,8 @@ from bot.utils.decorators import seasonal_task
log = logging.getLogger(__name__)
+EGG_FACTS = loads(Path("bot/resources/easter/easter_egg_facts.json").read_text("utf8"))
+
class EasterFacts(commands.Cog):
"""
@@ -22,17 +24,8 @@ class EasterFacts(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.facts = self.load_json()
-
self.daily_fact_task = self.bot.loop.create_task(self.send_egg_fact_daily())
- @staticmethod
- def load_json() -> dict:
- """Load a list of easter egg facts from the resource JSON file."""
- p = Path("bot/resources/easter/easter_egg_facts.json")
- with p.open(encoding="utf8") as f:
- return load(f)
-
@seasonal_task(Month.APRIL)
async def send_egg_fact_daily(self) -> None:
"""A background task that sends an easter egg fact in the event channel everyday."""
@@ -41,21 +34,22 @@ class EasterFacts(commands.Cog):
channel = self.bot.get_channel(Channels.community_bot_commands)
await channel.send(embed=self.make_embed())
- @commands.command(name='eggfact', aliases=['fact'])
+ @commands.command(name="eggfact", aliases=("fact",))
async def easter_facts(self, ctx: commands.Context) -> None:
"""Get easter egg facts."""
embed = self.make_embed()
await ctx.send(embed=embed)
- def make_embed(self) -> discord.Embed:
+ @staticmethod
+ def make_embed() -> discord.Embed:
"""Makes a nice embed for the message to be sent."""
return discord.Embed(
colour=Colours.soft_red,
title="Easter Egg Fact",
- description=random.choice(self.facts)
+ description=random.choice(EGG_FACTS)
)
def setup(bot: Bot) -> None:
- """Easter Egg facts cog load."""
+ """Load the Easter Egg facts Cog."""
bot.add_cog(EasterFacts(bot))
diff --git a/bot/exts/easter/egghead_quiz.py b/bot/exts/easter/egghead_quiz.py
index 0498d9db..7c4960cd 100644
--- a/bot/exts/easter/egghead_quiz.py
+++ b/bot/exts/easter/egghead_quiz.py
@@ -1,28 +1,28 @@
import asyncio
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
from typing import Union
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/easter/egghead_questions.json"), "r", encoding="utf8") as f:
- EGGHEAD_QUESTIONS = load(f)
+EGGHEAD_QUESTIONS = loads(Path("bot/resources/easter/egghead_questions.json").read_text("utf8"))
EMOJIS = [
- '\U0001f1e6', '\U0001f1e7', '\U0001f1e8', '\U0001f1e9', '\U0001f1ea',
- '\U0001f1eb', '\U0001f1ec', '\U0001f1ed', '\U0001f1ee', '\U0001f1ef',
- '\U0001f1f0', '\U0001f1f1', '\U0001f1f2', '\U0001f1f3', '\U0001f1f4',
- '\U0001f1f5', '\U0001f1f6', '\U0001f1f7', '\U0001f1f8', '\U0001f1f9',
- '\U0001f1fa', '\U0001f1fb', '\U0001f1fc', '\U0001f1fd', '\U0001f1fe',
- '\U0001f1ff'
+ "\U0001f1e6", "\U0001f1e7", "\U0001f1e8", "\U0001f1e9", "\U0001f1ea",
+ "\U0001f1eb", "\U0001f1ec", "\U0001f1ed", "\U0001f1ee", "\U0001f1ef",
+ "\U0001f1f0", "\U0001f1f1", "\U0001f1f2", "\U0001f1f3", "\U0001f1f4",
+ "\U0001f1f5", "\U0001f1f6", "\U0001f1f7", "\U0001f1f8", "\U0001f1f9",
+ "\U0001f1fa", "\U0001f1fb", "\U0001f1fc", "\U0001f1fd", "\U0001f1fe",
+ "\U0001f1ff"
] # Regional Indicators A-Z (used for voting)
TIMELIMIT = 30
@@ -31,11 +31,10 @@ TIMELIMIT = 30
class EggheadQuiz(commands.Cog):
"""This cog contains the command for the Easter quiz!"""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
+ def __init__(self) -> None:
self.quiz_messages = {}
- @commands.command(aliases=["eggheadquiz", "easterquiz"])
+ @commands.command(aliases=("eggheadquiz", "easterquiz"))
async def eggquiz(self, ctx: commands.Context) -> None:
"""
Gives a random quiz question, waits 30 seconds and then outputs the answer.
@@ -64,7 +63,7 @@ class EggheadQuiz(commands.Cog):
del self.quiz_messages[msg.id]
- msg = await ctx.channel.fetch_message(msg.id) # Refreshes message
+ msg = await ctx.fetch_message(msg.id) # Refreshes message
total_no = sum([len(await r.users().flatten()) for r in msg.reactions]) - len(valid_emojis) # - bot's reactions
@@ -114,6 +113,6 @@ class EggheadQuiz(commands.Cog):
return await reaction.message.remove_reaction(reaction, user)
-def setup(bot: commands.Bot) -> None:
- """Egghead Quiz Cog load."""
- bot.add_cog(EggheadQuiz(bot))
+def setup(bot: Bot) -> None:
+ """Load the Egghead Quiz Cog."""
+ bot.add_cog(EggheadQuiz())
diff --git a/bot/exts/easter/save_the_planet.py b/bot/exts/easter/save_the_planet.py
index 8f644259..1bd515f2 100644
--- a/bot/exts/easter/save_the_planet.py
+++ b/bot/exts/easter/save_the_planet.py
@@ -4,26 +4,22 @@ from pathlib import Path
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.utils.randomization import RandomCycle
-
-with Path("bot/resources/easter/save_the_planet.json").open('r', encoding='utf8') as f:
- EMBED_DATA = RandomCycle(json.load(f))
+EMBED_DATA = RandomCycle(json.loads(Path("bot/resources/easter/save_the_planet.json").read_text("utf8")))
class SaveThePlanet(commands.Cog):
"""A cog that teaches users how they can help our planet."""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @commands.command(aliases=('savetheearth', 'saveplanet', 'saveearth'))
+ @commands.command(aliases=("savetheearth", "saveplanet", "saveearth"))
async def savetheplanet(self, ctx: commands.Context) -> None:
"""Responds with a random tip on how to be eco-friendly and help our planet."""
return_embed = Embed.from_dict(next(EMBED_DATA))
await ctx.send(embed=return_embed)
-def setup(bot: commands.Bot) -> None:
- """Save the Planet Cog load."""
- bot.add_cog(SaveThePlanet(bot))
+def setup(bot: Bot) -> None:
+ """Load the Save the Planet Cog."""
+ bot.add_cog(SaveThePlanet())
diff --git a/bot/exts/easter/traditions.py b/bot/exts/easter/traditions.py
index 85b4adfb..93404f3e 100644
--- a/bot/exts/easter/traditions.py
+++ b/bot/exts/easter/traditions.py
@@ -5,19 +5,17 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with open(Path("bot/resources/easter/traditions.json"), "r", encoding="utf8") as f:
- traditions = json.load(f)
+traditions = json.loads(Path("bot/resources/easter/traditions.json").read_text("utf8"))
class Traditions(commands.Cog):
"""A cog which allows users to get a random easter tradition or custom from a random country."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('eastercustoms',))
+ @commands.command(aliases=("eastercustoms",))
async def easter_tradition(self, ctx: commands.Context) -> None:
"""Responds with a random tradition or custom."""
random_country = random.choice(list(traditions))
@@ -25,6 +23,6 @@ class Traditions(commands.Cog):
await ctx.send(f"{random_country}:\n{traditions[random_country]}")
-def setup(bot: commands.Bot) -> None:
- """Traditions Cog load."""
- bot.add_cog(Traditions(bot))
+def setup(bot: Bot) -> None:
+ """Load the Traditions Cog."""
+ bot.add_cog(Traditions())
diff --git a/bot/exts/evergreen/avatar_modification/_effects.py b/bot/exts/evergreen/avatar_modification/_effects.py
index d2370b4b..b53b26f3 100644
--- a/bot/exts/evergreen/avatar_modification/_effects.py
+++ b/bot/exts/evergreen/avatar_modification/_effects.py
@@ -14,7 +14,7 @@ class PfpEffects:
"""
Implements various image modifying effects, for the PfpModify cog.
- All of these fuctions are slow, and blocking, so they should be ran in executors.
+ All of these functions are slow, and blocking, so they should be ran in executors.
"""
@staticmethod
@@ -102,7 +102,7 @@ class PfpEffects:
Applies the easter effect to the given image.
This is done by getting the closest "easter" colour to each pixel and changing the colour
- to the half-way RGBvalue.
+ to the half-way RGB value.
We also then add an overlay image on top in middle right, a chocolate bunny by default.
"""
@@ -251,7 +251,7 @@ class PfpEffects:
total_width = multiplier * single_wdith
total_height = multiplier * single_height
- new_image = Image.new('RGBA', (total_width, total_height), (250, 250, 250))
+ new_image = Image.new("RGBA", (total_width, total_height), (250, 250, 250))
width_multiplier = 0
height = 0
@@ -273,15 +273,15 @@ class PfpEffects:
@staticmethod
def mosaic_effect(img_bytes: bytes, squares: int, file_name: str) -> discord.File:
- """Seperate function run from an executor which turns an image into a mosaic."""
+ """Separate function run from an executor which turns an image into a mosaic."""
avatar = Image.open(BytesIO(img_bytes))
- avatar = avatar.convert('RGBA').resize((1024, 1024))
+ avatar = avatar.convert("RGBA").resize((1024, 1024))
img_squares = PfpEffects.split_image(avatar, squares)
new_img = PfpEffects.join_images(img_squares)
bufferedio = BytesIO()
- new_img.save(bufferedio, format='PNG')
+ new_img.save(bufferedio, format="PNG")
bufferedio.seek(0)
return discord.File(bufferedio, filename=file_name)
diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py
index 693d15c7..17f34ed4 100644
--- a/bot/exts/evergreen/avatar_modification/avatar_modify.py
+++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py
@@ -6,12 +6,13 @@ import string
import typing as t
import unicodedata
from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
import discord
from aiohttp import client_exceptions
from discord.ext import commands
-from discord.ext.commands.errors import BadArgument
+from bot.bot import Bot
from bot.constants import Colours, Emojis
from bot.exts.evergreen.avatar_modification._effects import PfpEffects
from bot.utils.extensions import invoke_help_command
@@ -27,13 +28,12 @@ MAX_SQUARES = 10_000
T = t.TypeVar("T")
-with open("bot/resources/pride/gender_options.json") as f:
- GENDER_OPTIONS = json.load(f)
+GENDER_OPTIONS = json.loads(Path("bot/resources/pride/gender_options.json").read_text("utf8"))
async def in_executor(func: t.Callable[..., T], *args) -> T:
"""
- Runs the given synchronus function `func` in an executor.
+ Runs the given synchronous function `func` in an executor.
This is useful for running slow, blocking code within async
functions, so that they don't block the bot.
@@ -63,7 +63,7 @@ def file_safe_name(effect: str, display_name: str) -> str:
class AvatarModify(commands.Cog):
"""Various commands for users to apply affects to their own avatars."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
async def _fetch_user(self, user_id: int) -> t.Optional[discord.User]:
@@ -71,7 +71,7 @@ class AvatarModify(commands.Cog):
Fetches a user and handles errors.
This helper function is required as the member cache doesn't always have the most up to date
- profile picture. This can lead to errors if the image is delted from the Discord CDN.
+ profile picture. This can lead to errors if the image is deleted from the Discord CDN.
fetch_member can't be used due to the avatar url being part of the user object, and
some weird caching that D.py does
"""
@@ -260,9 +260,9 @@ class AvatarModify(commands.Cog):
return
image_bytes = await response.read()
except client_exceptions.ClientConnectorError:
- raise BadArgument("Cannot connect to provided URL!")
+ raise commands.BadArgument("Cannot connect to provided URL!")
except client_exceptions.InvalidURL:
- raise BadArgument("Invalid URL!")
+ raise commands.BadArgument("Invalid URL!")
await self.send_pride_image(ctx, image_bytes, pixels, flag, option)
@@ -365,6 +365,6 @@ class AvatarModify(commands.Cog):
await ctx.send(file=file, embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the AvatarModify cog."""
bot.add_cog(AvatarModify(bot))
diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py
index 1681434f..c2f2079c 100644
--- a/bot/exts/evergreen/battleship.py
+++ b/bot/exts/evergreen/battleship.py
@@ -9,6 +9,7 @@ from functools import partial
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
@@ -30,8 +31,8 @@ EmojiSet = typing.Dict[typing.Tuple[bool, bool], str]
class Player:
"""Each player in the game - their messages for the boards and their current grid."""
- user: discord.Member
- board: discord.Message
+ user: typing.Optional[discord.Member]
+ board: typing.Optional[discord.Message]
opponent_board: discord.Message
grid: Grid
@@ -95,7 +96,7 @@ class Game:
def __init__(
self,
- bot: commands.Bot,
+ bot: Bot,
channel: discord.TextChannel,
player1: discord.Member,
player2: discord.Member
@@ -237,7 +238,7 @@ class Game:
square = None
turn_message = await self.turn.user.send(
"It's your turn! Type the square you want to fire at. Format it like this: A1\n"
- "Type `surrender` to give up"
+ "Type `surrender` to give up."
)
await self.next.user.send("Their turn", delete_after=3.0)
while True:
@@ -321,7 +322,7 @@ class Game:
class Battleship(commands.Cog):
"""Play the classic game Battleship!"""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.games: typing.List[Game] = []
self.waiting: typing.List[discord.Member] = []
@@ -378,10 +379,12 @@ class Battleship(commands.Cog):
Make sure you have your DMs open so that the bot can message you.
"""
if self.already_playing(ctx.author):
- return await ctx.send("You're already playing a game!")
+ await ctx.send("You're already playing a game!")
+ return
if ctx.author in self.waiting:
- return await ctx.send("You've already sent out a request for a player 2")
+ await ctx.send("You've already sent out a request for a player 2.")
+ return
announcement = await ctx.send(
"**Battleship**: A new game is about to start!\n"
@@ -401,20 +404,22 @@ class Battleship(commands.Cog):
except asyncio.TimeoutError:
self.waiting.remove(ctx.author)
await announcement.delete()
- return await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...")
+ await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...")
+ return
if str(reaction.emoji) == CROSS_EMOJI:
self.waiting.remove(ctx.author)
await announcement.delete()
- return await ctx.send(f"{ctx.author.mention} Game cancelled.")
+ await ctx.send(f"{ctx.author.mention} Game cancelled.")
+ return
await announcement.delete()
self.waiting.remove(ctx.author)
if self.already_playing(ctx.author):
return
+ game = Game(self.bot, ctx.channel, ctx.author, user)
+ self.games.append(game)
try:
- game = Game(self.bot, ctx.channel, ctx.author, user)
- self.games.append(game)
await game.start_game()
self.games.remove(game)
except discord.Forbidden:
@@ -425,11 +430,11 @@ class Battleship(commands.Cog):
self.games.remove(game)
except Exception:
# End the game in the event of an unforseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed")
+ await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed.")
self.games.remove(game)
raise
- @battleship.command(name="ships", aliases=["boats"])
+ @battleship.command(name="ships", aliases=("boats",))
async def battleship_ships(self, ctx: commands.Context) -> None:
"""Lists the ships that are found on the battleship grid."""
embed = discord.Embed(colour=Colours.blue)
@@ -438,6 +443,6 @@ class Battleship(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Battleship Cog."""
bot.add_cog(Battleship(bot))
diff --git a/bot/exts/evergreen/bookmark.py b/bot/exts/evergreen/bookmark.py
index 5fa05d2e..85c9b46f 100644
--- a/bot/exts/evergreen/bookmark.py
+++ b/bot/exts/evergreen/bookmark.py
@@ -1,34 +1,108 @@
+import asyncio
import logging
import random
+import typing as t
import discord
from discord.ext import commands
-from bot.constants import Colours, ERROR_REPLIES, Emojis, Icons
+from bot.bot import Bot
+from bot.constants import Colours, ERROR_REPLIES, Icons
from bot.utils.converters import WrappedMessageConverter
log = logging.getLogger(__name__)
+# Number of seconds to wait for other users to bookmark the same message
+TIMEOUT = 120
+BOOKMARK_EMOJI = "📌"
+
class Bookmark(commands.Cog):
"""Creates personal bookmarks by relaying a message link to the user's DMs."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
+ @staticmethod
+ def build_bookmark_dm(target_message: discord.Message, title: str) -> discord.Embed:
+ """Build the embed to DM the bookmark requester."""
+ embed = discord.Embed(
+ title=title,
+ description=target_message.content,
+ colour=Colours.soft_green
+ )
+ embed.add_field(
+ name="Wanna give it a visit?",
+ value=f"[Visit original message]({target_message.jump_url})"
+ )
+ embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url)
+ embed.set_thumbnail(url=Icons.bookmark)
+
+ return embed
+
+ @staticmethod
+ def build_error_embed(user: discord.Member) -> discord.Embed:
+ """Builds an error embed for when a bookmark requester has DMs disabled."""
+ return discord.Embed(
+ title=random.choice(ERROR_REPLIES),
+ description=f"{user.mention}, please enable your DMs to receive the bookmark.",
+ colour=Colours.soft_red
+ )
+
+ async def action_bookmark(
+ self,
+ channel: discord.TextChannel,
+ user: discord.Member,
+ target_message: discord.Message,
+ title: str
+ ) -> None:
+ """Sends the bookmark DM, or sends an error embed when a user bookmarks a message."""
+ try:
+ embed = self.build_bookmark_dm(target_message, title)
+ await user.send(embed=embed)
+ except discord.Forbidden:
+ error_embed = self.build_error_embed(user)
+ await channel.send(embed=error_embed)
+ else:
+ log.info(f"{user} bookmarked {target_message.jump_url} with title '{title}'")
+
+ @staticmethod
+ async def send_reaction_embed(
+ channel: discord.TextChannel,
+ target_message: discord.Message
+ ) -> discord.Message:
+ """Sends an embed, with a reaction, so users can react to bookmark the message too."""
+ message = await channel.send(
+ embed=discord.Embed(
+ description=(
+ f"React with {BOOKMARK_EMOJI} to be sent your very own bookmark to "
+ f"[this message]({target_message.jump_url})."
+ ),
+ colour=Colours.soft_green
+ )
+ )
+
+ await message.add_reaction(BOOKMARK_EMOJI)
+ return message
+
@commands.command(name="bookmark", aliases=("bm", "pin"))
async def bookmark(
self,
ctx: commands.Context,
- target_message: WrappedMessageConverter,
+ target_message: t.Optional[WrappedMessageConverter],
*,
title: str = "Bookmark"
) -> None:
"""Send the author a link to `target_message` via DMs."""
+ if not target_message:
+ if not ctx.message.reference:
+ raise commands.UserInputError("You must either provide a valid message to bookmark, or reply to one.")
+ target_message = ctx.message.reference.resolved
+
# Prevent users from bookmarking a message in a channel they don't have access to
permissions = ctx.author.permissions_in(target_message.channel)
if not permissions.read_messages:
- log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions")
+ log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions.")
embed = discord.Embed(
title=random.choice(ERROR_REPLIES),
color=Colours.soft_red,
@@ -37,29 +111,40 @@ class Bookmark(commands.Cog):
await ctx.send(embed=embed)
return
- embed = discord.Embed(
- title=title,
- colour=Colours.soft_green,
- description=target_message.content
- )
- embed.add_field(name="Wanna give it a visit?", value=f"[Visit original message]({target_message.jump_url})")
- embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url)
- embed.set_thumbnail(url=Icons.bookmark)
-
- try:
- await ctx.author.send(embed=embed)
- except discord.Forbidden:
- error_embed = discord.Embed(
- title=random.choice(ERROR_REPLIES),
- description=f"{ctx.author.mention}, please enable your DMs to receive the bookmark",
- colour=Colours.soft_red
+ def event_check(reaction: discord.Reaction, user: discord.Member) -> bool:
+ """Make sure that this reaction is what we want to operate on."""
+ return (
+ # Conditions for a successful pagination:
+ all((
+ # Reaction is on this message
+ reaction.message.id == reaction_message.id,
+ # User has not already bookmarked this message
+ user.id not in bookmarked_users,
+ # Reaction is the `BOOKMARK_EMOJI` emoji
+ str(reaction.emoji) == BOOKMARK_EMOJI,
+ # Reaction was not made by the Bot
+ user.id != self.bot.user.id
+ ))
)
- await ctx.send(embed=error_embed)
- else:
- log.info(f"{ctx.author} bookmarked {target_message.jump_url} with title '{title}'")
- await ctx.message.add_reaction(Emojis.envelope)
+ await self.action_bookmark(ctx.channel, ctx.author, target_message, title)
+
+ # Keep track of who has already bookmarked, so users can't spam reactions and cause loads of DMs
+ bookmarked_users = [ctx.author.id]
+ reaction_message = await self.send_reaction_embed(ctx.channel, target_message)
+
+ while True:
+ try:
+ _, user = await self.bot.wait_for("reaction_add", timeout=TIMEOUT, check=event_check)
+ except asyncio.TimeoutError:
+ log.debug("Timed out waiting for a reaction")
+ break
+ log.trace(f"{user} has successfully bookmarked from a reaction, attempting to DM them.")
+ await self.action_bookmark(ctx.channel, user, target_message, title)
+ bookmarked_users.append(user.id)
+
+ await reaction_message.delete()
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Bookmark cog."""
bot.add_cog(Bookmark(bot))
diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py
index a175602f..32dfae09 100644
--- a/bot/exts/evergreen/catify.py
+++ b/bot/exts/evergreen/catify.py
@@ -5,6 +5,7 @@ from typing import Optional
from discord import AllowedMentions, Embed, Forbidden
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Cats, Colours, NEGATIVE_REPLIES
from bot.utils import helpers
@@ -12,10 +13,7 @@ from bot.utils import helpers
class Catify(commands.Cog):
"""Cog for the catify command."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=["ᓚᘏᗢify", "ᓚᘏᗢ"])
+ @commands.command(aliases=("ᓚᘏᗢify", "ᓚᘏᗢ"))
@commands.cooldown(1, 5, commands.BucketType.user)
async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None:
"""
@@ -83,6 +81,6 @@ class Catify(commands.Cog):
)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Loads the catify cog."""
- bot.add_cog(Catify(bot))
+ bot.add_cog(Catify())
diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py
index 3fe709d5..ae7793c9 100644
--- a/bot/exts/evergreen/cheatsheet.py
+++ b/bot/exts/evergreen/cheatsheet.py
@@ -8,6 +8,7 @@ from discord.ext import commands
from discord.ext.commands import BucketType, Context
from bot import constants
+from bot.bot import Bot
from bot.constants import Categories, Channels, Colours, ERROR_REPLIES
from bot.utils.decorators import whitelist_override
@@ -23,17 +24,17 @@ Unknown cheat sheet. Please try to reformulate your query.
If the problem persists send a message in <#{Channels.dev_contrib}>
"""
-URL = 'https://cheat.sh/python/{search}'
+URL = "https://cheat.sh/python/{search}"
ESCAPE_TT = str.maketrans({"`": "\\`"})
ANSI_RE = re.compile(r"\x1b\[.*?m")
# We need to pass headers as curl otherwise it would default to aiohttp which would return raw html.
-HEADERS = {'User-Agent': 'curl/7.68.0'}
+HEADERS = {"User-Agent": "curl/7.68.0"}
class CheatSheet(commands.Cog):
"""Commands that sends a result of a cht.sh search in code blocks."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@staticmethod
@@ -60,14 +61,18 @@ class CheatSheet(commands.Cog):
body_space = min(1986 - len(url), 1000)
if len(body_text) > body_space:
- description = (f"**Result Of cht.sh**\n"
- f"```python\n{body_text[:body_space]}\n"
- f"... (truncated - too many lines)```\n"
- f"Full results: {url} ")
+ description = (
+ f"**Result Of cht.sh**\n"
+ f"```python\n{body_text[:body_space]}\n"
+ f"... (truncated - too many lines)```\n"
+ f"Full results: {url} "
+ )
else:
- description = (f"**Result Of cht.sh**\n"
- f"```python\n{body_text}```\n"
- f"{url}")
+ description = (
+ f"**Result Of cht.sh**\n"
+ f"```python\n{body_text}```\n"
+ f"{url}"
+ )
return False, description
@commands.command(
@@ -102,6 +107,6 @@ class CheatSheet(commands.Cog):
await ctx.send(content=description)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the CheatSheet cog."""
bot.add_cog(CheatSheet(bot))
diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py
index 7e3ec42b..5c82ffee 100644
--- a/bot/exts/evergreen/connect_four.py
+++ b/bot/exts/evergreen/connect_four.py
@@ -8,6 +8,7 @@ import emojis
from discord.ext import commands
from discord.ext.commands import guild_only
+from bot.bot import Bot
from bot.constants import Emojis
NUMBERS = list(Emojis.number_emojis.values())
@@ -21,13 +22,13 @@ class Game:
"""A Connect 4 Game."""
def __init__(
- self,
- bot: commands.Bot,
- channel: discord.TextChannel,
- player1: discord.Member,
- player2: typing.Optional[discord.Member],
- tokens: typing.List[str],
- size: int = 7
+ self,
+ bot: Bot,
+ channel: discord.TextChannel,
+ player1: discord.Member,
+ player2: typing.Optional[discord.Member],
+ tokens: typing.List[str],
+ size: int = 7
) -> None:
self.bot = bot
@@ -54,8 +55,8 @@ class Game:
async def print_grid(self) -> None:
"""Formats and outputs the Connect Four grid to the channel."""
title = (
- f'Connect 4: {self.player1.display_name}'
- f' VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}'
+ f"Connect 4: {self.player1.display_name}"
+ f" VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}"
)
rows = [" ".join(self.tokens[s] for s in row) for row in self.grid]
@@ -66,7 +67,7 @@ class Game:
if self.message:
await self.message.edit(embed=embed)
else:
- self.message = await self.channel.send(content='Loading...')
+ self.message = await self.channel.send(content="Loading...")
for emoji in self.unicode_numbers:
await self.message.add_reaction(emoji)
await self.message.add_reaction(CROSS_EMOJI)
@@ -180,7 +181,7 @@ class Game:
class AI:
"""The Computer Player for Single-Player games."""
- def __init__(self, bot: commands.Bot, game: Game) -> None:
+ def __init__(self, bot: Bot, game: Game) -> None:
self.game = game
self.mention = bot.user.mention
@@ -255,7 +256,7 @@ class AI:
class ConnectFour(commands.Cog):
"""Connect Four. The Classic Vertical Four-in-a-row Game!"""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.games: typing.List[Game] = []
self.waiting: typing.List[discord.Member] = []
@@ -276,27 +277,29 @@ class ConnectFour(commands.Cog):
return False
if not self.min_board_size <= board_size <= self.max_board_size:
- await ctx.send(f"{board_size} is not a valid board size. A valid board size is "
- f"between `{self.min_board_size}` and `{self.max_board_size}`.")
+ await ctx.send(
+ f"{board_size} is not a valid board size. A valid board size is "
+ f"between `{self.min_board_size}` and `{self.max_board_size}`."
+ )
return False
return True
def get_player(
- self,
- ctx: commands.Context,
- announcement: discord.Message,
- reaction: discord.Reaction,
- user: discord.Member
+ self,
+ ctx: commands.Context,
+ announcement: discord.Message,
+ reaction: discord.Reaction,
+ user: discord.Member
) -> bool:
"""Predicate checking the criteria for the announcement message."""
if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2
return True # Is dealt with later on
if (
- user.id not in (ctx.me.id, ctx.author.id)
- and str(reaction.emoji) == Emojis.hand_raised
- and reaction.message.id == announcement.id
+ user.id not in (ctx.me.id, ctx.author.id)
+ and str(reaction.emoji) == Emojis.hand_raised
+ and reaction.message.id == announcement.id
):
if self.already_playing(user):
self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!"))
@@ -313,9 +316,9 @@ class ConnectFour(commands.Cog):
return True
if (
- user.id == ctx.author.id
- and str(reaction.emoji) == CROSS_EMOJI
- and reaction.message.id == announcement.id
+ user.id == ctx.author.id
+ and str(reaction.emoji) == CROSS_EMOJI
+ and reaction.message.id == announcement.id
):
return True
return False
@@ -326,7 +329,7 @@ class ConnectFour(commands.Cog):
@staticmethod
def check_emojis(
- e1: EMOJI_CHECK, e2: EMOJI_CHECK
+ e1: EMOJI_CHECK, e2: EMOJI_CHECK
) -> typing.Tuple[bool, typing.Optional[str]]:
"""Validate the emojis, the user put."""
if isinstance(e1, str) and emojis.count(e1) != 1:
@@ -336,12 +339,12 @@ class ConnectFour(commands.Cog):
return True, None
async def _play_game(
- self,
- ctx: commands.Context,
- user: typing.Optional[discord.Member],
- board_size: int,
- emoji1: str,
- emoji2: str
+ self,
+ ctx: commands.Context,
+ user: typing.Optional[discord.Member],
+ board_size: int,
+ emoji1: str,
+ emoji2: str
) -> None:
"""Helper for playing a game of connect four."""
self.tokens = [":white_circle:", str(emoji1), str(emoji2)]
@@ -354,7 +357,7 @@ class ConnectFour(commands.Cog):
self.games.remove(game)
except Exception:
# End the game in the event of an unforeseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed")
+ await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed.")
if game in self.games:
self.games.remove(game)
raise
@@ -362,15 +365,15 @@ class ConnectFour(commands.Cog):
@guild_only()
@commands.group(
invoke_without_command=True,
- aliases=["4inarow", "connect4", "connectfour", "c4"],
+ aliases=("4inarow", "connect4", "connectfour", "c4"),
case_insensitive=True
)
async def connect_four(
- self,
- ctx: commands.Context,
- board_size: int = 7,
- emoji1: EMOJI_CHECK = "\U0001f535",
- emoji2: EMOJI_CHECK = "\U0001f534"
+ self,
+ ctx: commands.Context,
+ board_size: int = 7,
+ emoji1: EMOJI_CHECK = "\U0001f535",
+ emoji2: EMOJI_CHECK = "\U0001f534"
) -> None:
"""
Play the classic game of Connect Four with someone!
@@ -425,13 +428,13 @@ class ConnectFour(commands.Cog):
await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2))
@guild_only()
- @connect_four.command(aliases=["bot", "computer", "cpu"])
+ @connect_four.command(aliases=("bot", "computer", "cpu"))
async def ai(
- self,
- ctx: commands.Context,
- board_size: int = 7,
- emoji1: EMOJI_CHECK = "\U0001f535",
- emoji2: EMOJI_CHECK = "\U0001f534"
+ self,
+ ctx: commands.Context,
+ board_size: int = 7,
+ emoji1: EMOJI_CHECK = "\U0001f535",
+ emoji2: EMOJI_CHECK = "\U0001f534"
) -> None:
"""Play Connect Four against a computer player."""
check, emoji = self.check_emojis(emoji1, emoji2)
@@ -445,6 +448,6 @@ class ConnectFour(commands.Cog):
await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2))
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load ConnectFour Cog."""
bot.add_cog(ConnectFour(bot))
diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py
index e7058961..fdc4467a 100644
--- a/bot/exts/evergreen/conversationstarters.py
+++ b/bot/exts/evergreen/conversationstarters.py
@@ -4,11 +4,12 @@ import yaml
from discord import Color, Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import WHITELISTED_CHANNELS
from bot.utils.decorators import whitelist_override
from bot.utils.randomization import RandomCycle
-SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9'
+SUGGESTION_FORM = "https://forms.gle/zw6kkJqv8U43Nfjg9"
with Path("bot/resources/evergreen/starter.yaml").open("r", encoding="utf8") as f:
STARTERS = yaml.load(f, Loader=yaml.FullLoader)
@@ -24,9 +25,9 @@ with Path("bot/resources/evergreen/py_topics.yaml").open("r", encoding="utf8") a
ALL_ALLOWED_CHANNELS = list(PY_TOPICS.keys()) + list(WHITELISTED_CHANNELS)
# Putting all topics into one dictionary and shuffling lists to reduce same-topic repetitions.
-ALL_TOPICS = {'default': STARTERS, **PY_TOPICS}
+ALL_TOPICS = {"default": STARTERS, **PY_TOPICS}
TOPICS = {
- channel: RandomCycle(topics or ['No topics found for this channel.'])
+ channel: RandomCycle(topics or ["No topics found for this channel."])
for channel, topics in ALL_TOPICS.items()
}
@@ -34,9 +35,6 @@ TOPICS = {
class ConvoStarters(commands.Cog):
"""Evergreen conversation topics."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command()
@whitelist_override(channels=ALL_ALLOWED_CHANNELS)
async def topic(self, ctx: commands.Context) -> None:
@@ -48,7 +46,7 @@ class ConvoStarters(commands.Cog):
Otherwise, a random conversation topic will be received by the user.
"""
# No matter what, the form will be shown.
- embed = Embed(description=f'Suggest more topics [here]({SUGGESTION_FORM})!', color=Color.blurple())
+ embed = Embed(description=f"Suggest more topics [here]({SUGGESTION_FORM})!", color=Color.blurple())
try:
# Fetching topics.
@@ -56,16 +54,16 @@ class ConvoStarters(commands.Cog):
# If the channel isn't Python-related.
except KeyError:
- embed.title = f'**{next(TOPICS["default"])}**'
+ embed.title = f"**{next(TOPICS['default'])}**"
# If the channel ID doesn't have any topics.
else:
- embed.title = f'**{next(channel_topics)}**'
+ embed.title = f"**{next(channel_topics)}**"
finally:
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Conversation starters Cog load."""
- bot.add_cog(ConvoStarters(bot))
+def setup(bot: Bot) -> None:
+ """Load the ConvoStarters cog."""
+ bot.add_cog(ConvoStarters())
diff --git a/bot/exts/evergreen/emoji.py b/bot/exts/evergreen/emoji.py
index fa3044e3..11615214 100644
--- a/bot/exts/evergreen/emoji.py
+++ b/bot/exts/evergreen/emoji.py
@@ -8,6 +8,7 @@ from typing import List, Optional, Tuple
from discord import Color, Embed, Emoji
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours, ERROR_REPLIES
from bot.utils.extensions import invoke_help_command
from bot.utils.pagination import LinePaginator
@@ -19,9 +20,6 @@ log = logging.getLogger(__name__)
class Emojis(commands.Cog):
"""A collection of commands related to emojis in the server."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@staticmethod
def embed_builder(emoji: dict) -> Tuple[Embed, List[str]]:
"""Generates an embed with the emoji names and count."""
@@ -48,9 +46,9 @@ class Emojis(commands.Cog):
else:
emoji_info = f"There is **{len(category_emojis)}** emoji in the **{category_name}** category."
if emoji_choice.animated:
- msg.append(f'<a:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}')
+ msg.append(f"<a:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}")
else:
- msg.append(f'<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}')
+ msg.append(f"<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}")
return embed, msg
@staticmethod
@@ -66,7 +64,7 @@ class Emojis(commands.Cog):
for emoji in emojis:
emoji_dict[emoji.name.split("_")[0]].append(emoji)
- error_comp = ', '.join(emoji_dict)
+ error_comp = ", ".join(emoji_dict)
msg.append(f"These are the valid emoji categories:\n```{error_comp}```")
return embed, msg
@@ -86,7 +84,7 @@ class Emojis(commands.Cog):
if not ctx.guild.emojis:
await ctx.send("No emojis found.")
return
- log.trace(f"Emoji Category {'' if category_query else 'not '}provided by the user")
+ log.trace(f"Emoji Category {'' if category_query else 'not '}provided by the user.")
for emoji in ctx.guild.emojis:
emoji_category = emoji.name.split("_")[0]
@@ -120,6 +118,6 @@ class Emojis(commands.Cog):
await ctx.send(embed=emoji_information)
-def setup(bot: commands.Bot) -> None:
- """Add the Emojis cog into the bot."""
- bot.add_cog(Emojis(bot))
+def setup(bot: Bot) -> None:
+ """Load the Emojis cog."""
+ bot.add_cog(Emojis())
diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index 8db49748..de8e53d0 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -7,6 +7,7 @@ from discord import Embed, Message
from discord.ext import commands
from sentry_sdk import push_scope
+from bot.bot import Bot
from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import UserNotPlayingError
@@ -17,9 +18,6 @@ log = logging.getLogger(__name__)
class CommandErrorHandler(commands.Cog):
"""A error handler for the PythonDiscord server."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@staticmethod
def revert_cooldown_counter(command: commands.Command, message: Message) -> None:
"""Undoes the last cooldown counter for user-error cases."""
@@ -41,8 +39,8 @@ class CommandErrorHandler(commands.Cog):
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context, error: commands.CommandError) -> None:
- """Activates when a command opens an error."""
- if getattr(error, 'handled', False):
+ """Activates when a command raises an error."""
+ if getattr(error, "handled", False):
logging.debug(f"Command {ctx.command} had its error already handled locally; ignoring.")
return
@@ -51,7 +49,7 @@ class CommandErrorHandler(commands.Cog):
parent_command = f"{ctx.command} "
ctx = subctx
- error = getattr(error, 'original', error)
+ error = getattr(error, "original", error)
logging.debug(
f"Error Encountered: {type(error).__name__} - {str(error)}, "
f"Command: {ctx.command}, "
@@ -127,14 +125,11 @@ class CommandErrorHandler(commands.Cog):
scope.set_extra("full_message", ctx.message.content)
if ctx.guild is not None:
- scope.set_extra(
- "jump_to",
- f"https://discordapp.com/channels/{ctx.guild.id}/{ctx.channel.id}/{ctx.message.id}"
- )
+ scope.set_extra("jump_to", ctx.message.jump_url)
log.exception(f"Unhandled command error: {str(error)}", exc_info=error)
-def setup(bot: commands.Bot) -> None:
- """Error handler Cog load."""
- bot.add_cog(CommandErrorHandler(bot))
+def setup(bot: Bot) -> None:
+ """Load the ErrorHandler cog."""
+ bot.add_cog(CommandErrorHandler())
diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py
index 7152d0cb..3b266e1b 100644
--- a/bot/exts/evergreen/fun.py
+++ b/bot/exts/evergreen/fun.py
@@ -7,9 +7,10 @@ from typing import Callable, Iterable, Tuple, Union
from discord import Embed, Message
from discord.ext import commands
-from discord.ext.commands import BadArgument, Bot, Cog, Context, MessageConverter, clean_content
+from discord.ext.commands import BadArgument, Cog, Context, MessageConverter, clean_content
from bot import utils
+from bot.bot import Bot
from bot.constants import Client, Colours, Emojis
from bot.utils import helpers
@@ -55,8 +56,7 @@ class Fun(Cog):
def __init__(self, bot: Bot) -> None:
self.bot = bot
- with Path("bot/resources/evergreen/caesar_info.json").open("r", encoding="UTF-8") as f:
- self._caesar_cipher_embed = json.load(f)
+ self._caesar_cipher_embed = json.loads(Path("bot/resources/evergreen/caesar_info.json").read_text("UTF-8"))
@staticmethod
def _get_random_die() -> str:
@@ -242,6 +242,6 @@ class Fun(Cog):
return Embed.from_dict(embed_dict)
-def setup(bot: commands.Bot) -> None:
- """Fun Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Fun cog."""
bot.add_cog(Fun(bot))
diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py
index 068d3f68..32fe9263 100644
--- a/bot/exts/evergreen/game.py
+++ b/bot/exts/evergreen/game.py
@@ -176,7 +176,7 @@ class Games(Cog):
"Invalid OAuth credentials. Unloading Games cog. "
f"OAuth response message: {result['message']}"
)
- self.bot.remove_cog('Games')
+ self.bot.remove_cog("Games")
return
@@ -224,8 +224,8 @@ class Games(Cog):
else:
self.genres[genre_name] = genre
- @group(name="games", aliases=["game"], invoke_without_command=True)
- async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str] = None) -> None:
+ @group(name="games", aliases=("game",), invoke_without_command=True)
+ async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str]) -> None:
"""
Get random game(s) by genre from IGDB. Use .games genres command to get all available genres.
@@ -277,7 +277,7 @@ class Games(Cog):
await ImagePaginator.paginate(pages, ctx, Embed(title=f"Random {genre.title()} Games"))
- @games.command(name="top", aliases=["t"])
+ @games.command(name="top", aliases=("t",))
async def top(self, ctx: Context, amount: int = 10) -> None:
"""
Get current Top games in IGDB.
@@ -294,19 +294,19 @@ class Games(Cog):
pages = [await self.create_page(game) for game in games]
await ImagePaginator.paginate(pages, ctx, Embed(title=f"Top {amount} Games"))
- @games.command(name="genres", aliases=["genre", "g"])
+ @games.command(name="genres", aliases=("genre", "g"))
async def genres(self, ctx: Context) -> None:
"""Get all available genres."""
await ctx.send(f"Currently available genres: {', '.join(f'`{genre}`' for genre in self.genres)}")
- @games.command(name="search", aliases=["s"])
+ @games.command(name="search", aliases=("s",))
async def search(self, ctx: Context, *, search_term: str) -> None:
"""Find games by name."""
lines = await self.search_games(search_term)
await LinePaginator.paginate(lines, ctx, Embed(title=f"Game Search Results: {search_term}"), empty=False)
- @games.command(name="company", aliases=["companies"])
+ @games.command(name="company", aliases=("companies",))
async def company(self, ctx: Context, amount: int = 5) -> None:
"""
Get random Game Companies companies from IGDB API.
@@ -325,7 +325,7 @@ class Games(Cog):
await ImagePaginator.paginate(pages, ctx, Embed(title="Random Game Companies"))
@with_role(*STAFF_ROLES)
- @games.command(name="refresh", aliases=["r"])
+ @games.command(name="refresh", aliases=("r",))
async def refresh_genres_command(self, ctx: Context) -> None:
"""Refresh .games command genres."""
try:
@@ -335,13 +335,14 @@ class Games(Cog):
return
await ctx.send("Successfully refreshed genres.")
- async def get_games_list(self,
- amount: int,
- genre: Optional[str] = None,
- sort: Optional[str] = None,
- additional_body: str = "",
- offset: int = 0
- ) -> List[Dict[str, Any]]:
+ async def get_games_list(
+ self,
+ amount: int,
+ genre: Optional[str] = None,
+ sort: Optional[str] = None,
+ additional_body: str = "",
+ offset: int = 0
+ ) -> List[Dict[str, Any]]:
"""
Get list of games from IGDB API by parameters that is provided.
@@ -373,8 +374,10 @@ class Games(Cog):
release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?"
# Create Age Ratings value
- rating = ", ".join(f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}"
- for age in data["age_ratings"]) if "age_ratings" in data else "?"
+ rating = ", ".join(
+ f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}"
+ for age in data["age_ratings"]
+ ) if "age_ratings" in data else "?"
companies = [c["company"]["name"] for c in data["involved_companies"]] if "involved_companies" in data else "?"
@@ -471,7 +474,7 @@ class Games(Cog):
def setup(bot: Bot) -> None:
- """Add/Load Games cog."""
+ """Load the Games cog."""
# Check does IGDB API key exist, if not, log warning and don't load cog
if not Tokens.igdb_client_id:
logger.warning("No IGDB client ID. Not loading Games cog.")
diff --git a/bot/exts/evergreen/githubinfo.py b/bot/exts/evergreen/githubinfo.py
index c8a6b3f7..27e607e5 100644
--- a/bot/exts/evergreen/githubinfo.py
+++ b/bot/exts/evergreen/githubinfo.py
@@ -5,8 +5,8 @@ from urllib.parse import quote
import discord
from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from bot.bot import Bot
from bot.constants import Colours, NEGATIVE_REPLIES
from bot.exts.utils.extensions import invoke_help_command
@@ -18,7 +18,7 @@ GITHUB_API_URL = "https://api.github.com"
class GithubInfo(commands.Cog):
"""Fetches info from GitHub."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
async def fetch_data(self, url: str) -> dict:
@@ -26,14 +26,14 @@ class GithubInfo(commands.Cog):
async with self.bot.http_session.get(url) as r:
return await r.json()
- @commands.group(name='github', aliases=('gh', 'git'))
- @commands.cooldown(1, 10, BucketType.user)
+ @commands.group(name="github", aliases=("gh", "git"))
+ @commands.cooldown(1, 10, commands.BucketType.user)
async def github_group(self, ctx: commands.Context) -> None:
"""Commands for finding information related to GitHub."""
if ctx.invoked_subcommand is None:
await invoke_help_command(ctx)
- @github_group.command(name='user', aliases=('userinfo',))
+ @github_group.command(name="user", aliases=("userinfo",))
async def github_user_info(self, ctx: commands.Context, username: str) -> None:
"""Fetches a user's GitHub information."""
async with ctx.typing():
@@ -50,31 +50,31 @@ class GithubInfo(commands.Cog):
await ctx.send(embed=embed)
return
- org_data = await self.fetch_data(user_data['organizations_url'])
+ org_data = await self.fetch_data(user_data["organizations_url"])
orgs = [f"[{org['login']}](https://github.com/{org['login']})" for org in org_data]
- orgs_to_add = ' | '.join(orgs)
+ orgs_to_add = " | ".join(orgs)
- gists = user_data['public_gists']
+ gists = user_data["public_gists"]
# Forming blog link
- if user_data['blog'].startswith("http"): # Blog link is complete
- blog = user_data['blog']
- elif user_data['blog']: # Blog exists but the link is not complete
+ if user_data["blog"].startswith("http"): # Blog link is complete
+ blog = user_data["blog"]
+ elif user_data["blog"]: # Blog exists but the link is not complete
blog = f"https://{user_data['blog']}"
else:
blog = "No website link available"
embed = discord.Embed(
title=f"`{user_data['login']}`'s GitHub profile info",
- description=f"```{user_data['bio']}```\n" if user_data['bio'] is not None else "",
+ description=f"```{user_data['bio']}```\n" if user_data["bio"] else "",
colour=discord.Colour.blurple(),
- url=user_data['html_url'],
- timestamp=datetime.strptime(user_data['created_at'], "%Y-%m-%dT%H:%M:%SZ")
+ url=user_data["html_url"],
+ timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ")
)
- embed.set_thumbnail(url=user_data['avatar_url'])
+ embed.set_thumbnail(url=user_data["avatar_url"])
embed.set_footer(text="Account created at")
- if user_data['type'] == "User":
+ if user_data["type"] == "User":
embed.add_field(
name="Followers",
@@ -90,12 +90,12 @@ class GithubInfo(commands.Cog):
value=f"[{user_data['public_repos']}]({user_data['html_url']}?tab=repositories)"
)
- if user_data['type'] == "User":
+ if user_data["type"] == "User":
embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{quote(username, safe='')})")
embed.add_field(
name=f"Organization{'s' if len(orgs)!=1 else ''}",
- value=orgs_to_add if orgs else "No organizations"
+ value=orgs_to_add if orgs else "No organizations."
)
embed.add_field(name="Website", value=blog)
@@ -108,8 +108,8 @@ class GithubInfo(commands.Cog):
The repository should look like `user/reponame` or `user reponame`.
"""
- repo = '/'.join(repo)
- if repo.count('/') != 1:
+ repo = "/".join(repo)
+ if repo.count("/") != 1:
embed = discord.Embed(
title=random.choice(NEGATIVE_REPLIES),
description="The repository should look like `user/reponame` or `user reponame`.",
@@ -134,10 +134,10 @@ class GithubInfo(commands.Cog):
return
embed = discord.Embed(
- title=repo_data['name'],
+ title=repo_data["name"],
description=repo_data["description"],
colour=discord.Colour.blurple(),
- url=repo_data['html_url']
+ url=repo_data["html_url"]
)
# If it's a fork, then it will have a parent key
@@ -147,7 +147,7 @@ class GithubInfo(commands.Cog):
except KeyError:
log.debug("Repository is not a fork.")
- repo_owner = repo_data['owner']
+ repo_owner = repo_data["owner"]
embed.set_author(
name=repo_owner["login"],
@@ -155,8 +155,8 @@ class GithubInfo(commands.Cog):
icon_url=repo_owner["avatar_url"]
)
- repo_created_at = datetime.strptime(repo_data['created_at'], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y")
- last_pushed = datetime.strptime(repo_data['pushed_at'], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y at %H:%M")
+ repo_created_at = datetime.strptime(repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y")
+ last_pushed = datetime.strptime(repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y at %H:%M")
embed.set_footer(
text=(
@@ -170,6 +170,6 @@ class GithubInfo(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Adding the cog to the bot."""
+def setup(bot: Bot) -> None:
+ """Load the GithubInfo cog."""
bot.add_cog(GithubInfo(bot))
diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py
index f557e42e..3c9ba4d2 100644
--- a/bot/exts/evergreen/help.py
+++ b/bot/exts/evergreen/help.py
@@ -2,9 +2,8 @@
import asyncio
import itertools
import logging
-from collections import namedtuple
from contextlib import suppress
-from typing import Union
+from typing import List, NamedTuple, Union
from discord import Colour, Embed, HTTPException, Message, Reaction, User
from discord.ext import commands
@@ -22,14 +21,21 @@ from bot.utils.pagination import (
DELETE_EMOJI = Emojis.trashcan
REACTIONS = {
- FIRST_EMOJI: 'first',
- LEFT_EMOJI: 'back',
- RIGHT_EMOJI: 'next',
- LAST_EMOJI: 'end',
- DELETE_EMOJI: 'stop',
+ FIRST_EMOJI: "first",
+ LEFT_EMOJI: "back",
+ RIGHT_EMOJI: "next",
+ LAST_EMOJI: "end",
+ DELETE_EMOJI: "stop",
}
-Cog = namedtuple('Cog', ['name', 'description', 'commands'])
+
+class Cog(NamedTuple):
+ """Show information about a Cog's name, description and commands."""
+
+ name: str
+ description: str
+ commands: List[Command]
+
log = logging.getLogger(__name__)
@@ -87,7 +93,7 @@ class HelpSession:
# set the query details for the session
if command:
- query_str = ' '.join(command)
+ query_str = " ".join(command)
self.query = self._get_query(query_str)
self.description = self.query.description or self.query.help
else:
@@ -191,7 +197,7 @@ class HelpSession:
self.reset_timeout()
# Run relevant action method
- action = getattr(self, f'do_{REACTIONS[emoji]}', None)
+ action = getattr(self, f"do_{REACTIONS[emoji]}", None)
if action:
await action()
@@ -234,11 +240,11 @@ class HelpSession:
if cmd.cog:
try:
if cmd.cog.category:
- return f'**{cmd.cog.category}**'
+ return f"**{cmd.cog.category}**"
except AttributeError:
pass
- return f'**{cmd.cog_name}**'
+ return f"**{cmd.cog_name}**"
else:
return "**\u200bNo Category:**"
@@ -262,141 +268,143 @@ class HelpSession:
# if default is not an empty string or None
if show_default:
- results.append(f'[{name}={param.default}]')
+ results.append(f"[{name}={param.default}]")
else:
- results.append(f'[{name}]')
+ results.append(f"[{name}]")
# if variable length argument
elif param.kind == param.VAR_POSITIONAL:
- results.append(f'[{name}...]')
+ results.append(f"[{name}...]")
# if required
else:
- results.append(f'<{name}>')
+ results.append(f"<{name}>")
return f"{cmd.name} {' '.join(results)}"
async def build_pages(self) -> None:
"""Builds the list of content pages to be paginated through in the help message, as a list of str."""
# Use LinePaginator to restrict embed line height
- paginator = LinePaginator(prefix='', suffix='', max_lines=self._max_lines)
-
- prefix = constants.Client.prefix
+ paginator = LinePaginator(prefix="", suffix="", max_lines=self._max_lines)
# show signature if query is a command
if isinstance(self.query, commands.Command):
- signature = self._get_command_params(self.query)
- parent = self.query.full_parent_name + ' ' if self.query.parent else ''
- paginator.add_line(f'**```{prefix}{parent}{signature}```**')
-
- aliases = [f"`{alias}`" if not parent else f"`{parent} {alias}`" for alias in self.query.aliases]
- aliases += [f"`{alias}`" for alias in getattr(self.query, "root_aliases", ())]
- aliases = ", ".join(sorted(aliases))
- if aliases:
- paginator.add_line(f'**Can also use:** {aliases}\n')
-
- if not await self.query.can_run(self._ctx):
- paginator.add_line('***You cannot run this command.***\n')
+ await self._add_command_signature(paginator)
if isinstance(self.query, Cog):
- paginator.add_line(f'**{self.query.name}**')
+ paginator.add_line(f"**{self.query.name}**")
if self.description:
- paginator.add_line(f'*{self.description}*')
+ paginator.add_line(f"*{self.description}*")
# list all children commands of the queried object
if isinstance(self.query, (commands.GroupMixin, Cog)):
+ await self._list_child_commands(paginator)
- # remove hidden commands if session is not wanting hiddens
- if not self._show_hidden:
- filtered = [c for c in self.query.commands if not c.hidden]
- else:
- filtered = self.query.commands
-
- # if after filter there are no commands, finish up
- if not filtered:
- self._pages = paginator.pages
- return
-
- if isinstance(self.query, Cog):
- grouped = (('**Commands:**', self.query.commands),)
-
- elif isinstance(self.query, commands.Command):
- grouped = (('**Subcommands:**', self.query.commands),)
-
- # don't show prefix for subcommands
- prefix = ''
+ self._pages = paginator.pages
- # otherwise sort and organise all commands into categories
- else:
- cat_sort = sorted(filtered, key=self._category_key)
- grouped = itertools.groupby(cat_sort, key=self._category_key)
+ async def _add_command_signature(self, paginator: LinePaginator) -> None:
+ prefix = constants.Client.prefix
- for category, cmds in grouped:
- cmds = sorted(cmds, key=lambda c: c.name)
+ signature = self._get_command_params(self.query)
+ parent = self.query.full_parent_name + " " if self.query.parent else ""
+ paginator.add_line(f"**```{prefix}{parent}{signature}```**")
+ aliases = [f"`{alias}`" if not parent else f"`{parent} {alias}`" for alias in self.query.aliases]
+ aliases += [f"`{alias}`" for alias in getattr(self.query, "root_aliases", ())]
+ aliases = ", ".join(sorted(aliases))
+ if aliases:
+ paginator.add_line(f"**Can also use:** {aliases}\n")
+ if not await self.query.can_run(self._ctx):
+ paginator.add_line("***You cannot run this command.***\n")
+
+ async def _list_child_commands(self, paginator: LinePaginator) -> None:
+ # remove hidden commands if session is not wanting hiddens
+ if not self._show_hidden:
+ filtered = [c for c in self.query.commands if not c.hidden]
+ else:
+ filtered = self.query.commands
- if len(cmds) == 0:
- continue
+ # if after filter there are no commands, finish up
+ if not filtered:
+ self._pages = paginator.pages
+ return
- cat_cmds = []
+ if isinstance(self.query, Cog):
+ grouped = (("**Commands:**", self.query.commands),)
- for command in cmds:
+ elif isinstance(self.query, commands.Command):
+ grouped = (("**Subcommands:**", self.query.commands),)
- # skip if hidden and hide if session is set to
- if command.hidden and not self._show_hidden:
- continue
+ # otherwise sort and organise all commands into categories
+ else:
+ cat_sort = sorted(filtered, key=self._category_key)
+ grouped = itertools.groupby(cat_sort, key=self._category_key)
- # see if the user can run the command
- strikeout = ''
+ for category, cmds in grouped:
+ await self._format_command_category(paginator, category, list(cmds))
- # Patch to make the !help command work outside of #bot-commands again
- # This probably needs a proper rewrite, but this will make it work in
- # the mean time.
- try:
- can_run = await command.can_run(self._ctx)
- except CheckFailure:
- can_run = False
+ async def _format_command_category(self, paginator: LinePaginator, category: str, cmds: List[Command]) -> None:
+ cmds = sorted(cmds, key=lambda c: c.name)
+ cat_cmds = []
+ for command in cmds:
+ cat_cmds += await self._format_command(command)
- if not can_run:
- # skip if we don't show commands they can't run
- if self._only_can_run:
- continue
- strikeout = '~~'
+ # state var for if the category should be added next
+ print_cat = 1
+ new_page = True
- signature = self._get_command_params(command)
- info = f"{strikeout}**`{prefix}{signature}`**{strikeout}"
+ for details in cat_cmds:
- # handle if the command has no docstring
- if command.short_doc:
- cat_cmds.append(f'{info}\n*{command.short_doc}*')
- else:
- cat_cmds.append(f'{info}\n*No details provided.*')
+ # keep details together, paginating early if it won"t fit
+ lines_adding = len(details.split("\n")) + print_cat
+ if paginator._linecount + lines_adding > self._max_lines:
+ paginator._linecount = 0
+ new_page = True
+ paginator.close_page()
- # state var for if the category should be added next
+ # new page so print category title again
print_cat = 1
- new_page = True
- for details in cat_cmds:
+ if print_cat:
+ if new_page:
+ paginator.add_line("")
+ paginator.add_line(category)
+ print_cat = 0
+
+ paginator.add_line(details)
- # keep details together, paginating early if it won't fit
- lines_adding = len(details.split('\n')) + print_cat
- if paginator._linecount + lines_adding > self._max_lines:
- paginator._linecount = 0
- new_page = True
- paginator.close_page()
+ async def _format_command(self, command: Command) -> List[str]:
+ # skip if hidden and hide if session is set to
+ if command.hidden and not self._show_hidden:
+ return []
- # new page so print category title again
- print_cat = 1
+ # Patch to make the !help command work outside of #bot-commands again
+ # This probably needs a proper rewrite, but this will make it work in
+ # the mean time.
+ try:
+ can_run = await command.can_run(self._ctx)
+ except CheckFailure:
+ can_run = False
+
+ # see if the user can run the command
+ strikeout = ""
+ if not can_run:
+ # skip if we don't show commands they can't run
+ if self._only_can_run:
+ return []
+ strikeout = "~~"
- if print_cat:
- if new_page:
- paginator.add_line('')
- paginator.add_line(category)
- print_cat = 0
+ if isinstance(self.query, commands.Command):
+ prefix = ""
+ else:
+ prefix = constants.Client.prefix
- paginator.add_line(details)
+ signature = self._get_command_params(command)
+ info = f"{strikeout}**`{prefix}{signature}`**{strikeout}"
- self._pages = paginator.pages
+ # handle if the command has no docstring
+ short_doc = command.short_doc or "No details provided"
+ return [f"{info}\n*{short_doc}*"]
def embed_page(self, page_number: int = 0) -> Embed:
"""Returns an Embed with the requested page formatted within."""
@@ -412,7 +420,7 @@ class HelpSession:
page_count = len(self._pages)
if page_count > 1:
- embed.set_footer(text=f'Page {self._current_page+1} / {page_count}')
+ embed.set_footer(text=f"Page {self._current_page+1} / {page_count}")
return embed
@@ -496,7 +504,7 @@ class HelpSession:
class Help(DiscordCog):
"""Custom Embed Pagination Help feature."""
- @commands.command('help')
+ @commands.command("help")
async def new_help(self, ctx: Context, *commands) -> None:
"""Shows Command Help."""
try:
@@ -507,8 +515,8 @@ class Help(DiscordCog):
embed.title = str(error)
if error.possible_matches:
- matches = '\n'.join(error.possible_matches.keys())
- embed.description = f'**Did you mean:**\n`{matches}`'
+ matches = "\n".join(error.possible_matches.keys())
+ embed.description = f"**Did you mean:**\n`{matches}`"
await ctx.send(embed=embed)
@@ -519,7 +527,7 @@ def unload(bot: Bot) -> None:
This is run if the cog raises an exception on load, or if the extension is unloaded.
"""
- bot.remove_command('help')
+ bot.remove_command("help")
bot.add_command(bot._old_help)
@@ -534,8 +542,8 @@ def setup(bot: Bot) -> None:
If an exception is raised during the loading of the cog, `unload` will be called in order to
reinstate the original help command.
"""
- bot._old_help = bot.get_command('help')
- bot.remove_command('help')
+ bot._old_help = bot.get_command("help")
+ bot.remove_command("help")
try:
bot.add_cog(Help())
diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py
index a0316080..b67aa4a6 100644
--- a/bot/exts/evergreen/issues.py
+++ b/bot/exts/evergreen/issues.py
@@ -7,6 +7,7 @@ from dataclasses import dataclass
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import (
Categories,
Channels,
@@ -91,7 +92,7 @@ class IssueState:
class Issues(commands.Cog):
"""Cog that allows users to retrieve issues from GitHub."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.repos = []
@@ -157,13 +158,13 @@ class Issues(commands.Cog):
issue_url = json_data.get("html_url")
- return IssueState(repository, number, issue_url, json_data.get('title', ''), emoji)
+ return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji)
@staticmethod
def format_embed(
- results: t.List[t.Union[IssueState, FetchError]],
- user: str,
- repository: t.Optional[str] = None
+ results: t.List[t.Union[IssueState, FetchError]],
+ user: str,
+ repository: t.Optional[str] = None
) -> discord.Embed:
"""Take a list of IssueState or FetchError and format a Discord embed for them."""
description_list = []
@@ -176,7 +177,7 @@ class Issues(commands.Cog):
resp = discord.Embed(
colour=Colours.bright_green,
- description='\n'.join(description_list)
+ description="\n".join(description_list)
)
embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}"
@@ -186,11 +187,11 @@ class Issues(commands.Cog):
@whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES)
@commands.command(aliases=("pr",))
async def issue(
- self,
- ctx: commands.Context,
- numbers: commands.Greedy[int],
- repository: str = "sir-lancebot",
- user: str = "python-discord"
+ self,
+ ctx: commands.Context,
+ numbers: commands.Greedy[int],
+ repository: str = "sir-lancebot",
+ user: str = "python-discord"
) -> None:
"""Command to retrieve issue(s) from a GitHub repository."""
# Remove duplicates
@@ -269,6 +270,6 @@ class Issues(commands.Cog):
await message.channel.send(embed=resp)
-def setup(bot: commands.Bot) -> None:
- """Cog Retrieves Issues From Github."""
+def setup(bot: Bot) -> None:
+ """Load the Issues cog."""
bot.add_cog(Issues(bot))
diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py
index c4a8597c..36c7e0ab 100644
--- a/bot/exts/evergreen/latex.py
+++ b/bot/exts/evergreen/latex.py
@@ -9,6 +9,8 @@ import discord
import matplotlib.pyplot as plt
from discord.ext import commands
+from bot.bot import Bot
+
# configure fonts and colors for matplotlib
plt.rcParams.update(
{
@@ -89,6 +91,11 @@ class Latex(commands.Cog):
await ctx.send(file=discord.File(image, "latex.png"))
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Latex Cog."""
- bot.add_cog(Latex(bot))
+ # As we have resource issues on this cog,
+ # we have it currently disabled while we fix it.
+ import logging
+ logging.info("Latex cog is currently disabled. It won't be loaded.")
+ return
+ bot.add_cog(Latex())
diff --git a/bot/exts/evergreen/magic_8ball.py b/bot/exts/evergreen/magic_8ball.py
index f974e487..28ddcea0 100644
--- a/bot/exts/evergreen/magic_8ball.py
+++ b/bot/exts/evergreen/magic_8ball.py
@@ -5,27 +5,26 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
+ANSWERS = json.loads(Path("bot/resources/evergreen/magic8ball.json").read_text("utf8"))
+
class Magic8ball(commands.Cog):
"""A Magic 8ball command to respond to a user's question."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- with open(Path("bot/resources/evergreen/magic8ball.json"), "r", encoding="utf8") as file:
- self.answers = json.load(file)
-
@commands.command(name="8ball")
async def output_answer(self, ctx: commands.Context, *, question: str) -> None:
"""Return a Magic 8ball answer from answers list."""
if len(question.split()) >= 3:
- answer = random.choice(self.answers)
+ answer = random.choice(ANSWERS)
await ctx.send(answer)
else:
await ctx.send("Usage: .8ball <question> (minimum length of 3 eg: `will I win?`)")
-def setup(bot: commands.Bot) -> None:
- """Magic 8ball Cog load."""
- bot.add_cog(Magic8ball(bot))
+def setup(bot: Bot) -> None:
+ """Load the Magic8Ball Cog."""
+ bot.add_cog(Magic8ball())
diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py
index 3031debc..932358f9 100644
--- a/bot/exts/evergreen/minesweeper.py
+++ b/bot/exts/evergreen/minesweeper.py
@@ -6,7 +6,9 @@ from random import randint, random
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Client
+from bot.utils.converters import CoordinateConverter
from bot.utils.exceptions import UserNotPlayingError
from bot.utils.extensions import invoke_help_command
@@ -31,33 +33,6 @@ MESSAGE_MAPPING = {
log = logging.getLogger(__name__)
-class CoordinateConverter(commands.Converter):
- """Converter for Coordinates."""
-
- async def convert(self, ctx: commands.Context, coordinate: str) -> typing.Tuple[int, int]:
- """Take in a coordinate string and turn it into an (x, y) tuple."""
- if not 2 <= len(coordinate) <= 3:
- raise commands.BadArgument('Invalid co-ordinate provided')
-
- coordinate = coordinate.lower()
- if coordinate[0].isalpha():
- digit = coordinate[1:]
- letter = coordinate[0]
- else:
- digit = coordinate[:-1]
- letter = coordinate[-1]
-
- if not digit.isdigit():
- raise commands.BadArgument
-
- x = ord(letter) - ord('a')
- y = int(digit) - 1
-
- if (not 0 <= x <= 9) or (not 0 <= y <= 9):
- raise commands.BadArgument
- return x, y
-
-
GameBoard = typing.List[typing.List[typing.Union[str, int]]]
@@ -78,10 +53,10 @@ GamesDict = typing.Dict[int, Game]
class Minesweeper(commands.Cog):
"""Play a game of Minesweeper."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self) -> None:
self.games: GamesDict = {} # Store the currently running games
- @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True)
+ @commands.group(name="minesweeper", aliases=("ms",), invoke_without_command=True)
async def minesweeper_group(self, ctx: commands.Context) -> None:
"""Commands for Playing Minesweeper."""
await invoke_help_command(ctx)
@@ -148,7 +123,7 @@ class Minesweeper(commands.Cog):
f"Close the game with `{Client.prefix}ms end`\n"
)
except discord.errors.Forbidden:
- log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members")
+ log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members.")
await ctx.send(f":x: {ctx.author.mention}, please enable DMs to play minesweeper.")
return
@@ -158,7 +133,7 @@ class Minesweeper(commands.Cog):
dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}")
if ctx.guild:
- await ctx.send(f"{ctx.author.mention} is playing Minesweeper")
+ await ctx.send(f"{ctx.author.mention} is playing Minesweeper.")
chat_msg = await ctx.send(f"Here's their board!\n{self.format_for_discord(revealed_board)}")
else:
chat_msg = None
@@ -237,17 +212,17 @@ class Minesweeper(commands.Cog):
return True
async def reveal_one(
- self,
- ctx: commands.Context,
- revealed: GameBoard,
- board: GameBoard,
- x: int,
- y: int
+ self,
+ ctx: commands.Context,
+ revealed: GameBoard,
+ board: GameBoard,
+ x: int,
+ y: int
) -> bool:
"""
Reveal one square.
- return is True if the game ended, breaking the loop in `reveal_command` and deleting the game
+ return is True if the game ended, breaking the loop in `reveal_command` and deleting the game.
"""
revealed[y][x] = board[y][x]
if board[y][x] == "bomb":
@@ -285,13 +260,13 @@ class Minesweeper(commands.Cog):
game = self.games[ctx.author.id]
game.revealed = game.board
await self.update_boards(ctx)
- new_msg = f":no_entry: Game canceled :no_entry:\n{game.dm_msg.content}"
+ new_msg = f":no_entry: Game canceled. :no_entry:\n{game.dm_msg.content}"
await game.dm_msg.edit(content=new_msg)
if game.activated_on_server:
await game.chat_msg.edit(content=new_msg)
del self.games[ctx.author.id]
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Minesweeper cog."""
- bot.add_cog(Minesweeper(bot))
+ bot.add_cog(Minesweeper())
diff --git a/bot/exts/evergreen/movie.py b/bot/exts/evergreen/movie.py
index b3bfe998..10638aea 100644
--- a/bot/exts/evergreen/movie.py
+++ b/bot/exts/evergreen/movie.py
@@ -6,8 +6,9 @@ from urllib.parse import urlencode
from aiohttp import ClientSession
from discord import Embed
-from discord.ext.commands import Bot, Cog, Context, group
+from discord.ext.commands import Cog, Context, group
+from bot.bot import Bot
from bot.constants import Tokens
from bot.utils.extensions import invoke_help_command
from bot.utils.pagination import ImagePaginator
@@ -50,10 +51,9 @@ class Movie(Cog):
"""Movie Cog contains movies command that grab random movies from TMDB."""
def __init__(self, bot: Bot):
- self.bot = bot
self.http_session: ClientSession = bot.http_session
- @group(name='movies', aliases=['movie'], invoke_without_command=True)
+ @group(name="movies", aliases=("movie",), invoke_without_command=True)
async def movies(self, ctx: Context, genre: str = "", amount: int = 5) -> None:
"""
Get random movies by specifying genre. Also support amount parameter, that define how much movies will be shown.
@@ -72,15 +72,17 @@ class Movie(Cog):
# Capitalize genre for getting data from Enum, get random page, send help when genre don't exist.
genre = genre.capitalize()
try:
- result = await self.get_movies_list(self.http_session, MovieGenres[genre].value, 1)
+ result = await self.get_movies_data(self.http_session, MovieGenres[genre].value, 1)
except KeyError:
await invoke_help_command(ctx)
return
# Check if "results" is in result. If not, throw error.
- if "results" not in result.keys():
- err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \
- f"{result['status_message']}."
+ if "results" not in result:
+ err_msg = (
+ f"There is problem while making TMDB API request. Response Code: {result['status_code']}, "
+ f"{result['status_message']}."
+ )
await ctx.send(err_msg)
logger.warning(err_msg)
@@ -88,8 +90,8 @@ class Movie(Cog):
page = random.randint(1, result["total_pages"])
# Get movies list from TMDB, check if results key in result. When not, raise error.
- movies = await self.get_movies_list(self.http_session, MovieGenres[genre].value, page)
- if 'results' not in movies.keys():
+ movies = await self.get_movies_data(self.http_session, MovieGenres[genre].value, page)
+ if "results" not in movies:
err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \
f"{result['status_message']}."
await ctx.send(err_msg)
@@ -101,12 +103,12 @@ class Movie(Cog):
await ImagePaginator.paginate(pages, ctx, embed)
- @movies.command(name='genres', aliases=['genre', 'g'])
+ @movies.command(name="genres", aliases=("genre", "g"))
async def genres(self, ctx: Context) -> None:
"""Show all currently available genres for .movies command."""
await ctx.send(f"Current available genres: {', '.join('`' + genre.name + '`' for genre in MovieGenres)}")
- async def get_movies_list(self, client: ClientSession, genre_id: str, page: int) -> Dict[str, Any]:
+ async def get_movies_data(self, client: ClientSession, genre_id: str, page: int) -> List[Dict[str, Any]]:
"""Return JSON of TMDB discover request."""
# Define params of request
params = {
@@ -130,7 +132,7 @@ class Movie(Cog):
pages = []
for i in range(amount):
- movie_id = movies['results'][i]['id']
+ movie_id = movies["results"][i]["id"]
movie = await self.get_movie(client, movie_id)
page, img = await self.create_page(movie)
@@ -151,7 +153,7 @@ class Movie(Cog):
# Add title + tagline (if not empty)
text += f"**{movie['title']}**\n"
- if movie['tagline']:
+ if movie["tagline"]:
text += f"{movie['tagline']}\n\n"
else:
text += "\n"
@@ -162,8 +164,8 @@ class Movie(Cog):
text += "__**Production Information**__\n"
- companies = movie['production_companies']
- countries = movie['production_countries']
+ companies = movie["production_companies"]
+ countries = movie["production_countries"]
text += f"**Made by:** {', '.join(company['name'] for company in companies)}\n"
text += f"**Made in:** {', '.join(country['name'] for country in countries)}\n\n"
@@ -173,8 +175,8 @@ class Movie(Cog):
budget = f"{movie['budget']:,d}" if movie['budget'] else "?"
revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?"
- if movie['runtime'] is not None:
- duration = divmod(movie['runtime'], 60)
+ if movie["runtime"] is not None:
+ duration = divmod(movie["runtime"], 60)
else:
duration = ("?", "?")
@@ -182,7 +184,7 @@ class Movie(Cog):
text += f"**Revenue:** ${revenue}\n"
text += f"**Duration:** {f'{duration[0]} hour(s) {duration[1]} minute(s)'}\n\n"
- text += movie['overview']
+ text += movie["overview"]
img = f"http://image.tmdb.org/t/p/w200{movie['poster_path']}"
@@ -198,5 +200,5 @@ class Movie(Cog):
def setup(bot: Bot) -> None:
- """Load Movie Cog."""
+ """Load the Movie Cog."""
bot.add_cog(Movie(bot))
diff --git a/bot/exts/evergreen/ping.py b/bot/exts/evergreen/ping.py
index 07c13524..6be78117 100644
--- a/bot/exts/evergreen/ping.py
+++ b/bot/exts/evergreen/ping.py
@@ -4,13 +4,14 @@ from discord import Embed
from discord.ext import commands
from bot import start_time
+from bot.bot import Bot
from bot.constants import Colours
class Ping(commands.Cog):
"""Get info about the bot's ping and uptime."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.command(name="ping")
@@ -39,6 +40,6 @@ class Ping(commands.Cog):
await ctx.send(f"I started up {uptime_string}.")
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Ping cog."""
bot.add_cog(Ping(bot))
diff --git a/bot/exts/evergreen/pythonfacts.py b/bot/exts/evergreen/pythonfacts.py
index 457c2fd3..80a8da5d 100644
--- a/bot/exts/evergreen/pythonfacts.py
+++ b/bot/exts/evergreen/pythonfacts.py
@@ -3,31 +3,34 @@ import itertools
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
-with open('bot/resources/evergreen/python_facts.txt') as file:
+with open("bot/resources/evergreen/python_facts.txt") as file:
FACTS = itertools.cycle(list(file))
COLORS = itertools.cycle([Colours.python_blue, Colours.python_yellow])
+PYFACTS_DISCUSSION = "https://github.com/python-discord/meta/discussions/93"
class PythonFacts(commands.Cog):
"""Sends a random fun fact about Python."""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @commands.command(name='pythonfact', aliases=['pyfact'])
+ @commands.command(name="pythonfact", aliases=("pyfact",))
async def get_python_fact(self, ctx: commands.Context) -> None:
"""Sends a Random fun fact about Python."""
- embed = discord.Embed(title='Python Facts',
- description=next(FACTS),
- colour=next(COLORS))
- embed.add_field(name='Suggestions',
- value="Suggest more facts [here!](https://github.com/python-discord/meta/discussions/93)")
+ embed = discord.Embed(
+ title="Python Facts",
+ description=next(FACTS),
+ colour=next(COLORS)
+ )
+ embed.add_field(
+ name="Suggestions",
+ value=f"Suggest more facts [here!]({PYFACTS_DISCUSSION})"
+ )
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Load PythonFacts Cog."""
- bot.add_cog(PythonFacts(bot))
+def setup(bot: Bot) -> None:
+ """Load the PythonFacts Cog."""
+ bot.add_cog(PythonFacts())
diff --git a/bot/exts/evergreen/recommend_game.py b/bot/exts/evergreen/recommend_game.py
index 5e262a5b..35d60128 100644
--- a/bot/exts/evergreen/recommend_game.py
+++ b/bot/exts/evergreen/recommend_game.py
@@ -6,13 +6,14 @@ from random import shuffle
import discord
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
game_recs = []
# Populate the list `game_recs` with resource files
for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"):
- with rec_path.open(encoding='utf8') as file:
- data = json.load(file)
+ data = json.loads(rec_path.read_text("utf8"))
game_recs.append(data)
shuffle(game_recs)
@@ -20,11 +21,11 @@ shuffle(game_recs)
class RecommendGame(commands.Cog):
"""Commands related to recommending games."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.index = 0
- @commands.command(name="recommendgame", aliases=['gamerec'])
+ @commands.command(name="recommendgame", aliases=("gamerec",))
async def recommend_game(self, ctx: commands.Context) -> None:
"""Sends an Embed of a random game recommendation."""
if self.index >= len(game_recs):
@@ -33,18 +34,18 @@ class RecommendGame(commands.Cog):
game = game_recs[self.index]
self.index += 1
- author = self.bot.get_user(int(game['author']))
+ author = self.bot.get_user(int(game["author"]))
# Creating and formatting Embed
embed = discord.Embed(color=discord.Colour.blue())
if author is not None:
embed.set_author(name=author.name, icon_url=author.avatar_url)
- embed.set_image(url=game['image'])
- embed.add_field(name='Recommendation: ' + game['title'] + '\n' + game['link'], value=game['description'])
+ embed.set_image(url=game["image"])
+ embed.add_field(name=f"Recommendation: {game['title']}\n{game['link']}", value=game["description"])
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Loads the RecommendGame cog."""
bot.add_cog(RecommendGame(bot))
diff --git a/bot/exts/evergreen/snakes/__init__.py b/bot/exts/evergreen/snakes/__init__.py
index bc42f0c2..7740429b 100644
--- a/bot/exts/evergreen/snakes/__init__.py
+++ b/bot/exts/evergreen/snakes/__init__.py
@@ -1,12 +1,11 @@
import logging
-from discord.ext import commands
-
+from bot.bot import Bot
from bot.exts.evergreen.snakes._snakes_cog import Snakes
log = logging.getLogger(__name__)
-def setup(bot: commands.Bot) -> None:
- """Snakes Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Snakes Cog."""
bot.add_cog(Snakes(bot))
diff --git a/bot/exts/evergreen/snakes/_converter.py b/bot/exts/evergreen/snakes/_converter.py
index eee248cf..26bde611 100644
--- a/bot/exts/evergreen/snakes/_converter.py
+++ b/bot/exts/evergreen/snakes/_converter.py
@@ -24,8 +24,8 @@ class Snake(Converter):
await self.build_list()
name = name.lower()
- if name == 'python':
- return 'Python (programming language)'
+ if name == "python":
+ return "Python (programming language)"
def get_potential(iterable: Iterable, *, threshold: int = 80) -> List[str]:
nonlocal name
@@ -47,12 +47,12 @@ class Snake(Converter):
if name.lower() in self.special_cases:
return self.special_cases.get(name.lower(), name.lower())
- names = {snake['name']: snake['scientific'] for snake in self.snakes}
+ names = {snake["name"]: snake["scientific"] for snake in self.snakes}
all_names = names.keys() | names.values()
timeout = len(all_names) * (3 / 4)
embed = discord.Embed(
- title='Found multiple choices. Please choose the correct one.', colour=0x59982F)
+ title="Found multiple choices. Please choose the correct one.", colour=0x59982F)
embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url)
name = await disambiguate(ctx, get_potential(all_names), timeout=timeout, embed=embed)
@@ -63,14 +63,11 @@ class Snake(Converter):
"""Build list of snakes from the static snake resources."""
# Get all the snakes
if cls.snakes is None:
- with (SNAKE_RESOURCES / "snake_names.json").open(encoding="utf8") as snakefile:
- cls.snakes = json.load(snakefile)
-
+ cls.snakes = json.loads((SNAKE_RESOURCES / "snake_names.json").read_text("utf8"))
# Get the special cases
if cls.special_cases is None:
- with (SNAKE_RESOURCES / "special_snakes.json").open(encoding="utf8") as snakefile:
- special_cases = json.load(snakefile)
- cls.special_cases = {snake['name'].lower(): snake for snake in special_cases}
+ special_cases = json.loads((SNAKE_RESOURCES / "special_snakes.json").read_text("utf8"))
+ cls.special_cases = {snake["name"].lower(): snake for snake in special_cases}
@classmethod
async def random(cls) -> str:
@@ -81,5 +78,5 @@ class Snake(Converter):
so I can get it from here.
"""
await cls.build_list()
- names = [snake['scientific'] for snake in cls.snakes]
+ names = [snake["scientific"] for snake in cls.snakes]
return random.choice(names)
diff --git a/bot/exts/evergreen/snakes/_snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py
index 3732b559..07d3c363 100644
--- a/bot/exts/evergreen/snakes/_snakes_cog.py
+++ b/bot/exts/evergreen/snakes/_snakes_cog.py
@@ -9,15 +9,15 @@ import textwrap
import urllib
from functools import partial
from io import BytesIO
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
-import aiohttp
import async_timeout
from PIL import Image, ImageDraw, ImageFont
from discord import Colour, Embed, File, Member, Message, Reaction
from discord.errors import HTTPException
-from discord.ext.commands import Bot, Cog, CommandError, Context, bot_has_permissions, group
+from discord.ext.commands import Cog, CommandError, Context, bot_has_permissions, group
+from bot.bot import Bot
from bot.constants import ERROR_REPLIES, Tokens
from bot.exts.evergreen.snakes import _utils as utils
from bot.exts.evergreen.snakes._converter import Snake
@@ -143,8 +143,8 @@ class Snakes(Cog):
https://github.com/python-discord/code-jam-1
"""
- wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL)
- valid_image_extensions = ('gif', 'png', 'jpeg', 'jpg', 'webp')
+ wiki_brief = re.compile(r"(.*?)(=+ (.*?) =+)", flags=re.DOTALL)
+ valid_image_extensions = ("gif", "png", "jpeg", "jpg", "webp")
def __init__(self, bot: Bot):
self.active_sal = {}
@@ -183,28 +183,28 @@ class Snakes(Cog):
# Get the size of the snake icon, configure the height of the image box (yes, it changes)
icon_width = 347 # Hardcoded, not much i can do about that
icon_height = int((icon_width / snake.width) * snake.height)
- frame_copies = icon_height // CARD['frame'].height + 1
+ frame_copies = icon_height // CARD["frame"].height + 1
snake.thumbnail((icon_width, icon_height))
# Get the dimensions of the final image
- main_height = icon_height + CARD['top'].height + CARD['bottom'].height
- main_width = CARD['frame'].width
+ main_height = icon_height + CARD["top"].height + CARD["bottom"].height
+ main_width = CARD["frame"].width
# Start creating the foreground
foreground = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0))
- foreground.paste(CARD['top'], (0, 0))
+ foreground.paste(CARD["top"], (0, 0))
# Generate the frame borders to the correct height
for offset in range(frame_copies):
- position = (0, CARD['top'].height + offset * CARD['frame'].height)
- foreground.paste(CARD['frame'], position)
+ position = (0, CARD["top"].height + offset * CARD["frame"].height)
+ foreground.paste(CARD["frame"], position)
# Add the image and bottom part of the image
- foreground.paste(snake, (36, CARD['top'].height)) # Also hardcoded :(
- foreground.paste(CARD['bottom'], (0, CARD['top'].height + icon_height))
+ foreground.paste(snake, (36, CARD["top"].height)) # Also hardcoded :(
+ foreground.paste(CARD["bottom"], (0, CARD["top"].height + icon_height))
# Setup the background
- back = random.choice(CARD['backs'])
+ back = random.choice(CARD["backs"])
back_copies = main_height // back.height + 1
full_image = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0))
@@ -216,11 +216,11 @@ class Snakes(Cog):
full_image.paste(foreground, (0, 0), foreground)
# Get the first two sentences of the info
- description = '.'.join(content['info'].split(".")[:2]) + '.'
+ description = ".".join(content["info"].split(".")[:2]) + "."
# Setup positioning variables
margin = 36
- offset = CARD['top'].height + icon_height + margin
+ offset = CARD["top"].height + icon_height + margin
# Create blank rectangle image which will be behind the text
rectangle = Image.new(
@@ -242,12 +242,12 @@ class Snakes(Cog):
# Draw the text onto the final image
draw = ImageDraw.Draw(full_image)
for line in textwrap.wrap(description, 36):
- draw.text([margin + 4, offset], line, font=CARD['font'])
- offset += CARD['font'].getsize(line)[1]
+ draw.text([margin + 4, offset], line, font=CARD["font"])
+ offset += CARD["font"].getsize(line)[1]
# Get the image contents as a BufferIO object
buffer = BytesIO()
- full_image.save(buffer, 'PNG')
+ full_image.save(buffer, "PNG")
buffer.seek(0)
return buffer
@@ -275,13 +275,13 @@ class Snakes(Cog):
return message
- async def _fetch(self, session: aiohttp.ClientSession, url: str, params: dict = None) -> dict:
+ async def _fetch(self, url: str, params: Optional[dict] = None) -> dict:
"""Asynchronous web request helper method."""
if params is None:
params = {}
async with async_timeout.timeout(10):
- async with session.get(url, params=params) as response:
+ async with self.bot.http_session.get(url, params=params) as response:
return await response.json()
def _get_random_long_message(self, messages: List[str], retries: int = 10) -> str:
@@ -309,96 +309,95 @@ class Snakes(Cog):
"""
snake_info = {}
- async with aiohttp.ClientSession() as session:
- params = {
- 'format': 'json',
- 'action': 'query',
- 'list': 'search',
- 'srsearch': name,
- 'utf8': '',
- 'srlimit': '1',
- }
-
- json = await self._fetch(session, URL, params=params)
-
- # Wikipedia does have a error page
- try:
- pageid = json["query"]["search"][0]["pageid"]
- except KeyError:
- # Wikipedia error page ID(?)
- pageid = 41118
- except IndexError:
- return None
-
- params = {
- 'format': 'json',
- 'action': 'query',
- 'prop': 'extracts|images|info',
- 'exlimit': 'max',
- 'explaintext': '',
- 'inprop': 'url',
- 'pageids': pageid
- }
+ params = {
+ "format": "json",
+ "action": "query",
+ "list": "search",
+ "srsearch": name,
+ "utf8": "",
+ "srlimit": "1",
+ }
- json = await self._fetch(session, URL, params=params)
+ json = await self._fetch(URL, params=params)
- # Constructing dict - handle exceptions later
- try:
- snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"]
- snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"]
- snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"]
- snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"]
- snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"]
- except KeyError:
- snake_info["error"] = True
-
- if snake_info["images"]:
- i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/'
- image_list = []
- map_list = []
- thumb_list = []
-
- # Wikipedia has arbitrary images that are not snakes
- banned = [
- 'Commons-logo.svg',
- 'Red%20Pencil%20Icon.png',
- 'distribution',
- 'The%20Death%20of%20Cleopatra%20arthur.jpg',
- 'Head%20of%20holotype',
- 'locator',
- 'Woma.png',
- '-map.',
- '.svg',
- 'ange.',
- 'Adder%20(PSF).png'
- ]
-
- for image in snake_info["images"]:
- # Images come in the format of `File:filename.extension`
- file, sep, filename = image["title"].partition(':')
- filename = filename.replace(" ", "%20") # Wikipedia returns good data!
-
- if not filename.startswith('Map'):
- if any(ban in filename for ban in banned):
- pass
- else:
- image_list.append(f"{i_url}{filename}")
- thumb_list.append(f"{i_url}{filename}?width=100")
+ # Wikipedia does have a error page
+ try:
+ pageid = json["query"]["search"][0]["pageid"]
+ except KeyError:
+ # Wikipedia error page ID(?)
+ pageid = 41118
+ except IndexError:
+ return None
+
+ params = {
+ "format": "json",
+ "action": "query",
+ "prop": "extracts|images|info",
+ "exlimit": "max",
+ "explaintext": "",
+ "inprop": "url",
+ "pageids": pageid
+ }
+
+ json = await self._fetch(URL, params=params)
+
+ # Constructing dict - handle exceptions later
+ try:
+ snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"]
+ snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"]
+ snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"]
+ snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"]
+ snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"]
+ except KeyError:
+ snake_info["error"] = True
+
+ if snake_info["images"]:
+ i_url = "https://commons.wikimedia.org/wiki/Special:FilePath/"
+ image_list = []
+ map_list = []
+ thumb_list = []
+
+ # Wikipedia has arbitrary images that are not snakes
+ banned = [
+ "Commons-logo.svg",
+ "Red%20Pencil%20Icon.png",
+ "distribution",
+ "The%20Death%20of%20Cleopatra%20arthur.jpg",
+ "Head%20of%20holotype",
+ "locator",
+ "Woma.png",
+ "-map.",
+ ".svg",
+ "ange.",
+ "Adder%20(PSF).png"
+ ]
+
+ for image in snake_info["images"]:
+ # Images come in the format of `File:filename.extension`
+ file, sep, filename = image["title"].partition(":")
+ filename = filename.replace(" ", "%20") # Wikipedia returns good data!
+
+ if not filename.startswith("Map"):
+ if any(ban in filename for ban in banned):
+ pass
else:
- map_list.append(f"{i_url}{filename}")
+ image_list.append(f"{i_url}{filename}")
+ thumb_list.append(f"{i_url}{filename}?width=100")
+ else:
+ map_list.append(f"{i_url}{filename}")
- snake_info["image_list"] = image_list
- snake_info["map_list"] = map_list
- snake_info["thumb_list"] = thumb_list
- snake_info["name"] = name
+ snake_info["image_list"] = image_list
+ snake_info["map_list"] = map_list
+ snake_info["thumb_list"] = thumb_list
+ snake_info["name"] = name
- match = self.wiki_brief.match(snake_info['extract'])
- info = match.group(1) if match else None
+ match = self.wiki_brief.match(snake_info["extract"])
+ info = match.group(1) if match else None
- if info:
- info = info.replace("\n", "\n\n") # Give us some proper paragraphs.
+ if info:
+ info = info.replace("\n", "\n\n") # Give us some proper paragraphs.
- snake_info["info"] = info
+ snake_info["info"] = info
return snake_info
@@ -423,7 +422,7 @@ class Snakes(Cog):
try:
reaction, user = await ctx.bot.wait_for("reaction_add", timeout=45.0, check=predicate)
except asyncio.TimeoutError:
- await ctx.channel.send(f"You took too long. The correct answer was **{options[answer]}**.")
+ await ctx.send(f"You took too long. The correct answer was **{options[answer]}**.")
await message.clear_reactions()
return
@@ -438,13 +437,13 @@ class Snakes(Cog):
# endregion
# region: Commands
- @group(name='snakes', aliases=('snake',), invoke_without_command=True)
+ @group(name="snakes", aliases=("snake",), invoke_without_command=True)
async def snakes_group(self, ctx: Context) -> None:
"""Commands from our first code jam."""
await invoke_help_command(ctx)
@bot_has_permissions(manage_messages=True)
- @snakes_group.command(name='antidote')
+ @snakes_group.command(name="antidote")
@locked()
async def antidote_command(self, ctx: Context) -> None:
"""
@@ -498,9 +497,11 @@ class Snakes(Cog):
for i in range(0, 10):
page_guess_list.append(f"{HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI}")
page_result_list.append(f"{CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI}")
- board.append(f"`{i+1:02d}` "
- f"{page_guess_list[i]} - "
- f"{page_result_list[i]}")
+ board.append(
+ f"`{i+1:02d}` "
+ f"{page_guess_list[i]} - "
+ f"{page_result_list[i]}"
+ )
board.append(EMPTY_UNICODE)
antidote_embed.add_field(name="10 guesses remaining", value="\n".join(board))
board_id = await ctx.send(embed=antidote_embed) # Display board
@@ -578,15 +579,19 @@ class Snakes(Cog):
antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote")
antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url)
antidote_embed.set_image(url="https://media.giphy.com/media/ceeN6U57leAhi/giphy.gif")
- antidote_embed.add_field(name=EMPTY_UNICODE,
- value=f"Sorry you didnt make the antidote in time.\n"
- f"The formula was {' '.join(antidote_answer)}")
+ antidote_embed.add_field(
+ name=EMPTY_UNICODE,
+ value=(
+ f"Sorry you didnt make the antidote in time.\n"
+ f"The formula was {' '.join(antidote_answer)}"
+ )
+ )
await board_id.edit(embed=antidote_embed)
log.debug("Ending pagination and removing all reactions...")
await board_id.clear_reactions()
- @snakes_group.command(name='draw')
+ @snakes_group.command(name="draw")
async def draw_command(self, ctx: Context) -> None:
"""
Draws a random snek using Perlin noise.
@@ -621,10 +626,10 @@ class Snakes(Cog):
bg_color=bg_color
)
png_bytes = utils.frame_to_png_bytes(image_frame)
- file = File(png_bytes, filename='snek.png')
+ file = File(png_bytes, filename="snek.png")
await ctx.send(file=file)
- @snakes_group.command(name='get')
+ @snakes_group.command(name="get")
@bot_has_permissions(manage_messages=True)
@locked()
async def get_command(self, ctx: Context, *, name: Snake = None) -> None:
@@ -642,8 +647,9 @@ class Snakes(Cog):
else:
data = await self._get_snek(name)
- if data.get('error'):
- return await ctx.send('Could not fetch data from Wikipedia.')
+ if data.get("error"):
+ await ctx.send("Could not fetch data from Wikipedia.")
+ return
description = data["info"]
@@ -661,19 +667,25 @@ class Snakes(Cog):
# Build and send the embed.
embed = Embed(
- title=data.get("title", data.get('name')),
+ title=data.get("title", data.get("name")),
description=description,
colour=0x59982F,
)
- emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png'
- image = next((url for url in data['image_list']
- if url.endswith(self.valid_image_extensions)), emoji)
+ emoji = "https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png"
+
+ _iter = (
+ url
+ for url in data["image_list"]
+ if url.endswith(self.valid_image_extensions)
+ )
+ image = next(_iter, emoji)
+
embed.set_image(url=image)
await ctx.send(embed=embed)
- @snakes_group.command(name='guess', aliases=('identify',))
+ @snakes_group.command(name="guess", aliases=("identify",))
@locked()
async def guess_command(self, ctx: Context) -> None:
"""
@@ -693,11 +705,15 @@ class Snakes(Cog):
data = await self._get_snek(snake)
- image = next((url for url in data['image_list']
- if url.endswith(self.valid_image_extensions)), None)
+ _iter = (
+ url
+ for url in data["image_list"]
+ if url.endswith(self.valid_image_extensions)
+ )
+ image = next(_iter, None)
embed = Embed(
- title='Which of the following is the snake in the image?',
+ title="Which of the following is the snake in the image?",
description="\n".join(
f"{'ABCD'[snakes.index(snake)]}: {snake}" for snake in snakes),
colour=SNAKE_COLOR
@@ -708,7 +724,7 @@ class Snakes(Cog):
options = {f"{'abcd'[snakes.index(snake)]}": snake for snake in snakes}
await self._validate_answer(ctx, guess, answer, options)
- @snakes_group.command(name='hatch')
+ @snakes_group.command(name="hatch")
async def hatch_command(self, ctx: Context) -> None:
"""
Hatches your personal snake.
@@ -720,7 +736,7 @@ class Snakes(Cog):
snake_image = utils.snakes[snake_name]
# Hatch the snake
- message = await ctx.channel.send(embed=Embed(description="Hatching your snake :snake:..."))
+ message = await ctx.send(embed=Embed(description="Hatching your snake :snake:..."))
await asyncio.sleep(1)
for stage in utils.stages:
@@ -734,12 +750,12 @@ class Snakes(Cog):
my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name))
my_snake_embed.set_thumbnail(url=snake_image)
my_snake_embed.set_footer(
- text=" Owner: {0}#{1}".format(ctx.message.author.name, ctx.message.author.discriminator)
+ text=" Owner: {0}#{1}".format(ctx.author.name, ctx.author.discriminator)
)
- await ctx.channel.send(embed=my_snake_embed)
+ await ctx.send(embed=my_snake_embed)
- @snakes_group.command(name='movie')
+ @snakes_group.command(name="movie")
async def movie_command(self, ctx: Context) -> None:
"""
Gets a random snake-related movie from TMDB.
@@ -800,12 +816,12 @@ class Snakes(Cog):
embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png")
try:
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
except HTTPException as err:
- await ctx.channel.send("An error occurred while fetching a snake-related movie!")
+ await ctx.send("An error occurred while fetching a snake-related movie!")
raise err from None
- @snakes_group.command(name='quiz')
+ @snakes_group.command(name="quiz")
@locked()
async def quiz_command(self, ctx: Context) -> None:
"""
@@ -828,10 +844,10 @@ class Snakes(Cog):
)
)
- quiz = await ctx.channel.send("", embed=embed)
+ quiz = await ctx.send(embed=embed)
await self._validate_answer(ctx, quiz, answer, options)
- @snakes_group.command(name='name', aliases=('name_gen',))
+ @snakes_group.command(name="name", aliases=("name_gen",))
async def name_command(self, ctx: Context, *, name: str = None) -> None:
"""
Snakifies a username.
@@ -855,7 +871,7 @@ class Snakes(Cog):
This was written by Iceman, and modified for inclusion into the bot by lemon.
"""
snake_name = await self._get_snake_name()
- snake_name = snake_name['name']
+ snake_name = snake_name["name"]
snake_prefix = ""
# Set aside every word in the snake name except the last.
@@ -900,9 +916,10 @@ class Snakes(Cog):
color=SNAKE_COLOR
)
- return await ctx.send(embed=embed)
+ await ctx.send(embed=embed)
+ return
- @snakes_group.command(name='sal')
+ @snakes_group.command(name="sal")
@locked()
async def sal_command(self, ctx: Context) -> None:
"""
@@ -921,7 +938,7 @@ class Snakes(Cog):
await game.open_game()
- @snakes_group.command(name='about')
+ @snakes_group.command(name="about")
async def about_command(self, ctx: Context) -> None:
"""Show an embed with information about the event, its participants, and its winners."""
contributors = [
@@ -964,9 +981,9 @@ class Snakes(Cog):
)
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
- @snakes_group.command(name='card')
+ @snakes_group.command(name="card")
async def card_command(self, ctx: Context, *, name: Snake = None) -> None:
"""
Create an interesting little card from a snake.
@@ -976,7 +993,7 @@ class Snakes(Cog):
# Get the snake data we need
if not name:
name_obj = await self._get_snake_name()
- name = name_obj['scientific']
+ name = name_obj["scientific"]
content = await self._get_snek(name)
elif isinstance(name, dict):
@@ -990,7 +1007,7 @@ class Snakes(Cog):
stream = BytesIO()
async with async_timeout.timeout(10):
- async with self.bot.http_session.get(content['image_list'][0]) as response:
+ async with self.bot.http_session.get(content["image_list"][0]) as response:
stream.write(await response.read())
stream.seek(0)
@@ -1001,10 +1018,10 @@ class Snakes(Cog):
# Send it!
await ctx.send(
f"A wild {content['name'].title()} appears!",
- file=File(final_buffer, filename=content['name'].replace(" ", "") + ".png")
+ file=File(final_buffer, filename=content["name"].replace(" ", "") + ".png")
)
- @snakes_group.command(name='fact')
+ @snakes_group.command(name="fact")
async def fact_command(self, ctx: Context) -> None:
"""
Gets a snake-related fact.
@@ -1018,9 +1035,9 @@ class Snakes(Cog):
color=SNAKE_COLOR,
description=question
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
- @snakes_group.command(name='snakify')
+ @snakes_group.command(name="snakify")
async def snakify_command(self, ctx: Context, *, message: str = None) -> None:
"""
How would I talk if I were a snake?
@@ -1033,14 +1050,14 @@ class Snakes(Cog):
"""
with ctx.typing():
embed = Embed()
- user = ctx.message.author
+ user = ctx.author
if not message:
# Get a random message from the users history
messages = []
- async for message in ctx.channel.history(limit=500).filter(
- lambda msg: msg.author == ctx.message.author # Message was sent by author.
+ async for message in ctx.history(limit=500).filter(
+ lambda msg: msg.author == ctx.author # Message was sent by author.
):
messages.append(message.content)
@@ -1059,9 +1076,9 @@ class Snakes(Cog):
)
embed.description = f"*{self._snakify(message)}*"
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
- @snakes_group.command(name='video', aliases=('get_video',))
+ @snakes_group.command(name="video", aliases=("get_video",))
async def video_command(self, ctx: Context, *, search: str = None) -> None:
"""
Gets a YouTube video about snakes.
@@ -1072,13 +1089,13 @@ class Snakes(Cog):
"""
# Are we searching for anything specific?
if search:
- query = search + ' snake'
+ query = search + " snake"
else:
snake = await self._get_snake_name()
- query = snake['name']
+ query = snake["name"]
# Build the URL and make the request
- url = 'https://www.googleapis.com/youtube/v3/search'
+ url = "https://www.googleapis.com/youtube/v3/search"
response = await self.bot.http_session.get(
url,
params={
@@ -1094,14 +1111,14 @@ class Snakes(Cog):
# Send the user a video
if len(data) > 0:
num = random.randint(0, len(data) - 1)
- youtube_base_url = 'https://www.youtube.com/watch?v='
- await ctx.channel.send(
+ youtube_base_url = "https://www.youtube.com/watch?v="
+ await ctx.send(
content=f"{youtube_base_url}{data[num]['id']['videoId']}"
)
else:
log.warning(f"YouTube API error. Full response looks like {response}")
- @snakes_group.command(name='zen')
+ @snakes_group.command(name="zen")
async def zen_command(self, ctx: Context) -> None:
"""
Gets a random quote from the Zen of Python, except as if spoken by a snake.
@@ -1120,7 +1137,7 @@ class Snakes(Cog):
# Embed and send
embed.description = zen_quote
- await ctx.channel.send(
+ await ctx.send(
embed=embed
)
# endregion
diff --git a/bot/exts/evergreen/snakes/_utils.py b/bot/exts/evergreen/snakes/_utils.py
index 7d6caf04..0a5894b7 100644
--- a/bot/exts/evergreen/snakes/_utils.py
+++ b/bot/exts/evergreen/snakes/_utils.py
@@ -17,38 +17,38 @@ from bot.constants import Roles
SNAKE_RESOURCES = Path("bot/resources/snakes").absolute()
-h1 = r'''```
+h1 = r"""```
----
------
/--------\
|--------|
|--------|
\------/
- ----```'''
-h2 = r'''```
+ ----```"""
+h2 = r"""```
----
------
/---\-/--\
|-----\--|
|--------|
\------/
- ----```'''
-h3 = r'''```
+ ----```"""
+h3 = r"""```
----
------
/---\-/--\
|-----\--|
|-----/--|
\----\-/
- ----```'''
-h4 = r'''```
+ ----```"""
+h4 = r"""```
-----
----- \
/--| /---\
|--\ -\---|
|--\--/-- /
\------- /
- ------```'''
+ ------```"""
stages = [h1, h2, h3, h4]
snakes = {
"Baby Python": "https://i.imgur.com/SYOcmSa.png",
@@ -114,8 +114,7 @@ ANGLE_RANGE = math.pi * 2
def get_resource(file: str) -> List[dict]:
"""Load Snake resources JSON."""
- with (SNAKE_RESOURCES / f"{file}.json").open(encoding="utf-8") as snakefile:
- return json.load(snakefile)
+ return json.loads((SNAKE_RESOURCES / f"{file}.json").read_text("utf-8"))
def smoothstep(t: float) -> float:
@@ -191,8 +190,9 @@ class PerlinNoiseFactory(object):
def get_plain_noise(self, *point) -> float:
"""Get plain noise for a single point, without taking into account either octaves or tiling."""
if len(point) != self.dimension:
- raise ValueError("Expected {0} values, got {1}".format(
- self.dimension, len(point)))
+ raise ValueError(
+ f"Expected {self.dimension} values, got {len(point)}"
+ )
# Build a list of the (min, max) bounds in each dimension
grid_coords = []
@@ -321,7 +321,7 @@ def create_snek_frame(
image_dimensions[Y] / 2 - (dimension_range[Y] / 2 + min_dimensions[Y])
)
- image = Image.new(mode='RGB', size=image_dimensions, color=bg_color)
+ image = Image.new(mode="RGB", size=image_dimensions, color=bg_color)
draw = ImageDraw(image)
for index in range(1, len(points)):
point = points[index]
@@ -345,7 +345,7 @@ def create_snek_frame(
def frame_to_png_bytes(image: Image) -> io.BytesIO:
"""Convert image to byte stream."""
stream = io.BytesIO()
- image.save(stream, format='PNG')
+ image.save(stream, format="PNG")
stream.seek(0)
return stream
@@ -373,7 +373,7 @@ class SnakeAndLaddersGame:
self.snakes = snakes
self.ctx = context
self.channel = self.ctx.channel
- self.state = 'booting'
+ self.state = "booting"
self.started = False
self.author = self.ctx.author
self.players = []
@@ -413,7 +413,7 @@ class SnakeAndLaddersGame:
"**Snakes and Ladders**: A new game is about to start!",
file=File(
str(SNAKE_RESOURCES / "snakes_and_ladders" / "banner.jpg"),
- filename='Snakes and Ladders.jpg'
+ filename="Snakes and Ladders.jpg"
)
)
startup = await self.channel.send(
@@ -423,7 +423,7 @@ class SnakeAndLaddersGame:
for emoji in STARTUP_SCREEN_EMOJI:
await startup.add_reaction(emoji)
- self.state = 'waiting'
+ self.state = "waiting"
while not self.started:
try:
@@ -460,7 +460,7 @@ class SnakeAndLaddersGame:
self.players.append(user)
self.player_tiles[user.id] = 1
- avatar_bytes = await user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE).read()
+ avatar_bytes = await user.avatar_url_as(format="jpeg", size=PLAYER_ICON_IMAGE_SIZE).read()
im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE))
self.avatar_images[user.id] = im
@@ -475,7 +475,7 @@ class SnakeAndLaddersGame:
if user == p:
await self.channel.send(user.mention + " You are already in the game.", delete_after=10)
return
- if self.state != 'waiting':
+ if self.state != "waiting":
await self.channel.send(user.mention + " You cannot join at this time.", delete_after=10)
return
if len(self.players) is MAX_PLAYERS:
@@ -510,7 +510,7 @@ class SnakeAndLaddersGame:
delete_after=10
)
- if self.state != 'waiting' and len(self.players) == 0:
+ if self.state != "waiting" and len(self.players) == 0:
await self.channel.send("**Snakes and Ladders**: The game has been surrendered!")
is_surrendered = True
self._destruct()
@@ -535,12 +535,12 @@ class SnakeAndLaddersGame:
await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10)
return
- if not self.state == 'waiting':
+ if not self.state == "waiting":
await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10)
return
- self.state = 'starting'
- player_list = ', '.join(user.mention for user in self.players)
+ self.state = "starting"
+ player_list = ", ".join(user.mention for user in self.players)
await self.channel.send("**Snakes and Ladders**: The game is starting!\nPlayers: " + player_list)
await self.start_round()
@@ -556,10 +556,10 @@ class SnakeAndLaddersGame:
))
)
- self.state = 'roll'
+ self.state = "roll"
for user in self.players:
self.round_has_rolled[user.id] = False
- board_img = Image.open(str(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg"))
+ board_img = Image.open(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg")
player_row_size = math.ceil(MAX_PLAYERS / 2)
for i, player in enumerate(self.players):
@@ -574,8 +574,8 @@ class SnakeAndLaddersGame:
board_img.paste(self.avatar_images[player.id],
box=(x_offset, y_offset))
- board_file = File(frame_to_png_bytes(board_img), filename='Board.jpg')
- player_list = '\n'.join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players)
+ board_file = File(frame_to_png_bytes(board_img), filename="Board.jpg")
+ player_list = "\n".join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players)
# Store and send new messages
temp_board = await self.channel.send(
@@ -644,7 +644,7 @@ class SnakeAndLaddersGame:
if user.id not in self.player_tiles:
await self.channel.send(user.mention + " You are not in the match.", delete_after=10)
return
- if self.state != 'roll':
+ if self.state != "roll":
await self.channel.send(user.mention + " You may not roll at this time.", delete_after=10)
return
if self.round_has_rolled[user.id]:
@@ -673,7 +673,7 @@ class SnakeAndLaddersGame:
async def _complete_round(self) -> None:
"""At the conclusion of a round check to see if there's been a winner."""
- self.state = 'post_round'
+ self.state = "post_round"
# check for winner
winner = self._check_winner()
@@ -688,7 +688,7 @@ class SnakeAndLaddersGame:
def _check_winner(self) -> Member:
"""Return a winning member if we're in the post-round state and there's a winner."""
- if self.state != 'post_round':
+ if self.state != "post_round":
return None
return next((player for player in self.players if self.player_tiles[player.id] == 100),
None)
diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py
index 45752bf9..8fb72143 100644
--- a/bot/exts/evergreen/source.py
+++ b/bot/exts/evergreen/source.py
@@ -1,39 +1,18 @@
import inspect
from pathlib import Path
-from typing import Optional, Tuple, Union
+from typing import Optional, Tuple
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Source
-
-SourceType = Union[commands.Command, commands.Cog, str, commands.ExtensionNotLoaded]
-
-
-class SourceConverter(commands.Converter):
- """Convert an argument into a help command, tag, command, or cog."""
-
- async def convert(self, ctx: commands.Context, argument: str) -> SourceType:
- """Convert argument into source object."""
- cog = ctx.bot.get_cog(argument)
- if cog:
- return cog
-
- cmd = ctx.bot.get_command(argument)
- if cmd:
- return cmd
-
- raise commands.BadArgument(
- f"Unable to convert `{argument}` to valid command or Cog."
- )
+from bot.utils.converters import SourceConverter, SourceType
class BotSource(commands.Cog):
"""Displays information about the bot's source code."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command(name="source", aliases=("src",))
async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None:
"""Display information and a GitHub link to the source code of a command, tag, or cog."""
@@ -85,7 +64,7 @@ class BotSource(commands.Cog):
url, location, first_line = self.get_source_link(source_object)
if isinstance(source_object, commands.Command):
- if source_object.cog_name == 'Help':
+ if source_object.cog_name == "Help":
title = "Help Command"
description = source_object.__doc__.splitlines()[1]
else:
@@ -104,6 +83,6 @@ class BotSource(commands.Cog):
return embed
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the BotSource cog."""
- bot.add_cog(BotSource(bot))
+ bot.add_cog(BotSource())
diff --git a/bot/exts/evergreen/space.py b/bot/exts/evergreen/space.py
index 323ff659..5e87c6d5 100644
--- a/bot/exts/evergreen/space.py
+++ b/bot/exts/evergreen/space.py
@@ -1,15 +1,16 @@
import logging
import random
from datetime import date, datetime
-from typing import Any, Dict, Optional, Union
+from typing import Any, Dict, Optional
from urllib.parse import urlencode
from discord import Embed
from discord.ext import tasks
-from discord.ext.commands import BadArgument, Cog, Context, Converter, group
+from discord.ext.commands import Cog, Context, group
from bot.bot import Bot
from bot.constants import Tokens
+from bot.utils.converters import DateConverter
from bot.utils.extensions import invoke_help_command
logger = logging.getLogger(__name__)
@@ -21,25 +22,10 @@ NASA_EPIC_BASE_URL = "https://epic.gsfc.nasa.gov"
APOD_MIN_DATE = date(1995, 6, 16)
-class DateConverter(Converter):
- """Parse SOL or earth date (in format YYYY-MM-DD) into `int` or `datetime`. When invalid input, raise error."""
-
- async def convert(self, ctx: Context, argument: str) -> Union[int, datetime]:
- """Parse date (SOL or earth) into `datetime` or `int`. When invalid value, raise error."""
- if argument.isdigit():
- return int(argument)
- try:
- date = datetime.strptime(argument, "%Y-%m-%d")
- except ValueError:
- raise BadArgument(f"Can't convert `{argument}` to `datetime` in format `YYYY-MM-DD` or `int` in SOL.")
- return date
-
-
class Space(Cog):
"""Space Cog contains commands, that show images, facts or other information about space."""
def __init__(self, bot: Bot):
- self.bot = bot
self.http_session = bot.http_session
self.rovers = {}
@@ -67,7 +53,7 @@ class Space(Cog):
await invoke_help_command(ctx)
@space.command(name="apod")
- async def apod(self, ctx: Context, date: Optional[str] = None) -> None:
+ async def apod(self, ctx: Context, date: Optional[str]) -> None:
"""
Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD.
@@ -100,7 +86,7 @@ class Space(Cog):
)
@space.command(name="nasa")
- async def nasa(self, ctx: Context, *, search_term: Optional[str] = None) -> None:
+ async def nasa(self, ctx: Context, *, search_term: Optional[str]) -> None:
"""Get random NASA information/facts + image. Support `search_term` parameter for more specific search."""
params = {
"media_type": "image"
@@ -125,8 +111,8 @@ class Space(Cog):
)
@space.command(name="epic")
- async def epic(self, ctx: Context, date: Optional[str] = None) -> None:
- """Get one of latest random image of earth from NASA EPIC API. Support date parameter, format is YYYY-MM-DD."""
+ async def epic(self, ctx: Context, date: Optional[str]) -> None:
+ """Get a random image of the Earth from the NASA EPIC API. Support date parameter, format is YYYY-MM-DD."""
if date:
try:
show_date = datetime.strptime(date, "%Y-%m-%d").date().isoformat()
@@ -161,8 +147,8 @@ class Space(Cog):
async def mars(
self,
ctx: Context,
- date: Optional[DateConverter] = None,
- rover: Optional[str] = "curiosity"
+ date: Optional[DateConverter],
+ rover: str = "curiosity"
) -> None:
"""
Get random Mars image by date. Support both SOL (martian solar day) and earth date and rovers.
@@ -207,7 +193,7 @@ class Space(Cog):
)
)
- @mars.command(name="dates", aliases=["d", "date", "rover", "rovers", "r"])
+ @mars.command(name="dates", aliases=("d", "date", "rover", "rovers", "r"))
async def dates(self, ctx: Context) -> None:
"""Get current available rovers photo date ranges."""
await ctx.send("\n".join(
@@ -242,7 +228,7 @@ class Space(Cog):
def setup(bot: Bot) -> None:
- """Load Space Cog."""
+ """Load the Space cog."""
if not Tokens.nasa:
logger.warning("Can't find NASA API key. Not loading Space Cog.")
return
diff --git a/bot/exts/evergreen/speedrun.py b/bot/exts/evergreen/speedrun.py
index 21aad5aa..774eff81 100644
--- a/bot/exts/evergreen/speedrun.py
+++ b/bot/exts/evergreen/speedrun.py
@@ -5,23 +5,22 @@ from random import choice
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf8") as file:
- LINKS = json.load(file)
+
+LINKS = json.loads(Path("bot/resources/evergreen/speedrun_links.json").read_text("utf8"))
class Speedrun(commands.Cog):
"""Commands about the video game speedrunning community."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command(name="speedrun")
async def get_speedrun(self, ctx: commands.Context) -> None:
"""Sends a link to a video of a random speedrun."""
await ctx.send(choice(LINKS))
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Speedrun cog."""
- bot.add_cog(Speedrun(bot))
+ bot.add_cog(Speedrun())
diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py
index 0fe9ba47..dabf004d 100644
--- a/bot/exts/evergreen/status_codes.py
+++ b/bot/exts/evergreen/status_codes.py
@@ -4,6 +4,8 @@ from random import choice
import discord
from discord.ext import commands
+from bot.bot import Bot
+
HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg"
HTTP_CAT_URL = "https://http.cat/{code}.jpg"
@@ -15,7 +17,7 @@ class HTTPStatusCodes(commands.Cog):
If neither animal is selected a cat or dog is chosen randomly for the given status code.
"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.group(name="http_status", aliases=("status", "httpstatus"), invoke_without_command=True)
@@ -26,10 +28,10 @@ class HTTPStatusCodes(commands.Cog):
if await subcmd.can_run(ctx):
await subcmd(ctx, code)
- @http_status_group.command(name='cat')
+ @http_status_group.command(name="cat")
async def http_cat(self, ctx: commands.Context, code: int) -> None:
"""Sends an embed with an image of a cat, portraying the status code."""
- embed = discord.Embed(title=f'**Status: {code}**')
+ embed = discord.Embed(title=f"**Status: {code}**")
url = HTTP_CAT_URL.format(code=code)
try:
@@ -41,18 +43,18 @@ class HTTPStatusCodes(commands.Cog):
raise NotImplementedError
except ValueError:
- embed.set_footer(text='Inputted status code does not exist.')
+ embed.set_footer(text="Inputted status code does not exist.")
except NotImplementedError:
- embed.set_footer(text='Inputted status code is not implemented by http.cat yet.')
+ embed.set_footer(text="Inputted status code is not implemented by http.cat yet.")
finally:
await ctx.send(embed=embed)
- @http_status_group.command(name='dog')
+ @http_status_group.command(name="dog")
async def http_dog(self, ctx: commands.Context, code: int) -> None:
"""Sends an embed with an image of a dog, portraying the status code."""
- embed = discord.Embed(title=f'**Status: {code}**')
+ embed = discord.Embed(title=f"**Status: {code}**")
url = HTTP_DOG_URL.format(code=code)
try:
@@ -64,15 +66,15 @@ class HTTPStatusCodes(commands.Cog):
raise NotImplementedError
except ValueError:
- embed.set_footer(text='Inputted status code does not exist.')
+ embed.set_footer(text="Inputted status code does not exist.")
except NotImplementedError:
- embed.set_footer(text='Inputted status code is not implemented by httpstatusdogs.com yet.')
+ embed.set_footer(text="Inputted status code is not implemented by httpstatusdogs.com yet.")
finally:
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the HTTPStatusCodes cog."""
bot.add_cog(HTTPStatusCodes(bot))
diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py
index 6e21528e..bd5e0102 100644
--- a/bot/exts/evergreen/tic_tac_toe.py
+++ b/bot/exts/evergreen/tic_tac_toe.py
@@ -58,7 +58,7 @@ class Player:
)
try:
- react, _ = await self.ctx.bot.wait_for('reaction_add', timeout=30.0, check=check_for_move)
+ react, _ = await self.ctx.bot.wait_for("reaction_add", timeout=30.0, check=check_for_move)
except asyncio.TimeoutError:
return True, None
else:
@@ -246,8 +246,7 @@ def is_requester_free() -> t.Callable:
class TicTacToe(Cog):
"""TicTacToe cog contains tic-tac-toe game commands."""
- def __init__(self, bot: Bot):
- self.bot = bot
+ def __init__(self):
self.games: t.List[Game] = []
@guild_only()
@@ -323,5 +322,5 @@ class TicTacToe(Cog):
def setup(bot: Bot) -> None:
- """Load TicTacToe Cog."""
- bot.add_cog(TicTacToe(bot))
+ """Load the TicTacToe cog."""
+ bot.add_cog(TicTacToe())
diff --git a/bot/exts/evergreen/timed.py b/bot/exts/evergreen/timed.py
index 5f177fd6..2ea6b419 100644
--- a/bot/exts/evergreen/timed.py
+++ b/bot/exts/evergreen/timed.py
@@ -4,6 +4,8 @@ from time import perf_counter
from discord import Message
from discord.ext import commands
+from bot.bot import Bot
+
class TimedCommands(commands.Cog):
"""Time the command execution of a command."""
@@ -16,7 +18,7 @@ class TimedCommands(commands.Cog):
return await ctx.bot.get_context(msg)
- @commands.command(name="timed", aliases=["time", "t"])
+ @commands.command(name="timed", aliases=("time", "t"))
async def timed(self, ctx: commands.Context, *, command: str) -> None:
"""Time the command execution of a command."""
new_ctx = await self.create_execution_context(ctx, command)
@@ -41,6 +43,6 @@ class TimedCommands(commands.Cog):
await ctx.send(f"Command execution for `{new_ctx.command}` finished in {(t_end - t_start):.4f} seconds.")
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(TimedCommands(bot))
+def setup(bot: Bot) -> None:
+ """Load the Timed cog."""
+ bot.add_cog(TimedCommands())
diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py
index fe692c2a..419126dc 100644
--- a/bot/exts/evergreen/trivia_quiz.py
+++ b/bot/exts/evergreen/trivia_quiz.py
@@ -1,56 +1,235 @@
import asyncio
import json
import logging
+import operator
import random
+from dataclasses import dataclass
from pathlib import Path
+from typing import Callable, List, Optional
import discord
from discord.ext import commands
from fuzzywuzzy import fuzz
-from bot.constants import Roles
-
+from bot.bot import Bot
+from bot.constants import Colours, NEGATIVE_REPLIES, Roles
logger = logging.getLogger(__name__)
+DEFAULT_QUESTION_LIMIT = 6
+STANDARD_VARIATION_TOLERANCE = 83
+DYNAMICALLY_GEN_VARIATION_TOLERANCE = 95
WRONG_ANS_RESPONSE = [
"No one answered correctly!",
- "Better luck next time"
+ "Better luck next time...",
+]
+
+N_PREFIX_STARTS_AT = 5
+N_PREFIXES = [
+ "penta", "hexa", "hepta", "octa", "nona",
+ "deca", "hendeca", "dodeca", "trideca", "tetradeca",
+]
+
+PLANETS = [
+ ("1st", "Mercury"),
+ ("2nd", "Venus"),
+ ("3rd", "Earth"),
+ ("4th", "Mars"),
+ ("5th", "Jupiter"),
+ ("6th", "Saturn"),
+ ("7th", "Uranus"),
+ ("8th", "Neptune"),
+]
+
+TAXONOMIC_HIERARCHY = [
+ "species", "genus", "family", "order",
+ "class", "phylum", "kingdom", "domain",
]
+UNITS_TO_BASE_UNITS = {
+ "hertz": ("(unit of frequency)", "s^-1"),
+ "newton": ("(unit of force)", "m*kg*s^-2"),
+ "pascal": ("(unit of pressure & stress)", "m^-1*kg*s^-2"),
+ "joule": ("(unit of energy & quantity of heat)", "m^2*kg*s^-2"),
+ "watt": ("(unit of power)", "m^2*kg*s^-3"),
+ "coulomb": ("(unit of electric charge & quantity of electricity)", "s*A"),
+ "volt": ("(unit of voltage & electromotive force)", "m^2*kg*s^-3*A^-1"),
+ "farad": ("(unit of capacitance)", "m^-2*kg^-1*s^4*A^2"),
+ "ohm": ("(unit of electric resistance)", "m^2*kg*s^-3*A^-2"),
+ "weber": ("(unit of magnetic flux)", "m^2*kg*s^-2*A^-1"),
+ "tesla": ("(unit of magnetic flux density)", "kg*s^-2*A^-1"),
+}
+
+
+@dataclass(frozen=True)
+class QuizEntry:
+ """Dataclass for a quiz entry (a question and a string containing answers separated by commas)."""
+
+ question: str
+ answer: str
+
+
+def linear_system(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a system of linear equations with two unknowns."""
+ x, y = random.randint(2, 5), random.randint(2, 5)
+ answer = a_format.format(x, y)
+
+ coeffs = random.sample(range(1, 6), 4)
+
+ question = q_format.format(
+ coeffs[0],
+ coeffs[1],
+ coeffs[0] * x + coeffs[1] * y,
+ coeffs[2],
+ coeffs[3],
+ coeffs[2] * x + coeffs[3] * y,
+ )
+
+ return QuizEntry(question, answer)
+
+
+def mod_arith(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a basic modular arithmetic question."""
+ quotient, m, b = random.randint(30, 40), random.randint(10, 20), random.randint(200, 350)
+ ans = random.randint(0, 9) # max remainder is 9, since the minimum modulus is 10
+ a = quotient * m + ans - b
+
+ question = q_format.format(a, b, m)
+ answer = a_format.format(ans)
+
+ return QuizEntry(question, answer)
+
+
+def ngonal_prism(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question regarding vertices on n-gonal prisms."""
+ n = random.randint(0, len(N_PREFIXES) - 1)
+
+ question = q_format.format(N_PREFIXES[n])
+ answer = a_format.format((n + N_PREFIX_STARTS_AT) * 2)
+
+ return QuizEntry(question, answer)
+
+
+def imag_sqrt(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a negative square root question."""
+ ans_coeff = random.randint(3, 10)
+
+ question = q_format.format(ans_coeff ** 2)
+ answer = a_format.format(ans_coeff)
+
+ return QuizEntry(question, answer)
+
+
+def binary_calc(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a binary calculation question."""
+ a = random.randint(15, 20)
+ b = random.randint(10, a)
+ oper = random.choice(
+ (
+ ("+", operator.add),
+ ("-", operator.sub),
+ ("*", operator.mul),
+ )
+ )
+
+ # if the operator is multiplication, lower the values of the two operands to make it easier
+ if oper[0] == "*":
+ a -= 5
+ b -= 5
+
+ question = q_format.format(a, oper[0], b)
+ answer = a_format.format(oper[1](a, b))
+
+ return QuizEntry(question, answer)
+
+
+def solar_system(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question on the planets of the Solar System."""
+ planet = random.choice(PLANETS)
+
+ question = q_format.format(planet[0])
+ answer = a_format.format(planet[1])
+
+ return QuizEntry(question, answer)
+
+
+def taxonomic_rank(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question on taxonomic classification."""
+ level = random.randint(0, len(TAXONOMIC_HIERARCHY) - 2)
+
+ question = q_format.format(TAXONOMIC_HIERARCHY[level])
+ answer = a_format.format(TAXONOMIC_HIERARCHY[level + 1])
+
+ return QuizEntry(question, answer)
+
+
+def base_units_convert(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a SI base units conversion question."""
+ unit = random.choice(list(UNITS_TO_BASE_UNITS))
+
+ question = q_format.format(
+ unit + " " + UNITS_TO_BASE_UNITS[unit][0]
+ )
+ answer = a_format.format(
+ UNITS_TO_BASE_UNITS[unit][1]
+ )
+
+ return QuizEntry(question, answer)
+
+
+DYNAMIC_QUESTIONS_FORMAT_FUNCS = {
+ 201: linear_system,
+ 202: mod_arith,
+ 203: ngonal_prism,
+ 204: imag_sqrt,
+ 205: binary_calc,
+ 301: solar_system,
+ 302: taxonomic_rank,
+ 303: base_units_convert,
+}
+
class TriviaQuiz(commands.Cog):
"""A cog for all quiz commands."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
- self.questions = self.load_questions()
+
self.game_status = {} # A variable to store the game status: either running or not running.
self.game_owners = {} # A variable to store the person's ID who started the quiz game in a channel.
- self.question_limit = 4
+
+ self.questions = self.load_questions()
+ self.question_limit = 0
+
self.player_scores = {} # A variable to store all player's scores for a bot session.
self.game_player_scores = {} # A variable to store temporary game player's scores.
+
self.categories = {
- "general": "Test your general knowledge"
- # "retro": "Questions related to retro gaming."
+ "general": "Test your general knowledge.",
+ "retro": "Questions related to retro gaming.",
+ "math": "General questions about mathematics ranging from grade 8 to grade 12.",
+ "science": "Put your understanding of science to the test!",
}
@staticmethod
def load_questions() -> dict:
"""Load the questions from the JSON file."""
p = Path("bot", "resources", "evergreen", "trivia_quiz.json")
- with p.open(encoding="utf8") as json_data:
- questions = json.load(json_data)
- return questions
+
+ return json.loads(p.read_text(encoding="utf-8"))
@commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True)
- async def quiz_game(self, ctx: commands.Context, category: str = None) -> None:
+ async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None:
"""
Start a quiz!
Questions for the quiz can be selected from the following categories:
- - general : Test your general knowledge. (default)
+ - general: Test your general knowledge. (default)
+ - retro: Questions related to retro gaming.
+ - math: General questions about mathematics ranging from grade 8 to grade 12.
+ - science: Put your understanding of science to the test!
+
(More to come!)
"""
if ctx.channel.id not in self.game_status:
@@ -60,11 +239,12 @@ class TriviaQuiz(commands.Cog):
self.game_player_scores[ctx.channel.id] = {}
# Stop game if running.
- if self.game_status[ctx.channel.id] is True:
- return await ctx.send(
- f"Game is already running..."
+ if self.game_status[ctx.channel.id]:
+ await ctx.send(
+ "Game is already running... "
f"do `{self.bot.command_prefix}quiz stop`"
)
+ return
# Send embed showing available categories if inputted category is invalid.
if category is None:
@@ -76,8 +256,35 @@ class TriviaQuiz(commands.Cog):
await ctx.send(embed=embed)
return
+ topic = self.questions[category]
+ topic_length = len(topic)
+
+ if questions is None:
+ self.question_limit = DEFAULT_QUESTION_LIMIT
+ else:
+ if questions > topic_length:
+ await ctx.send(
+ embed=self.make_error_embed(
+ f"This category only has {topic_length} questions. "
+ "Please input a lower value!"
+ )
+ )
+ return
+
+ elif questions < 1:
+ await ctx.send(
+ embed=self.make_error_embed(
+ "You must choose to complete at least one question. "
+ f"(or enter nothing for the default value of {DEFAULT_QUESTION_LIMIT + 1} questions)"
+ )
+ )
+ return
+
+ else:
+ self.question_limit = questions - 1
+
# Start game if not running.
- if self.game_status[ctx.channel.id] is False:
+ if not self.game_status[ctx.channel.id]:
self.game_owners[ctx.channel.id] = ctx.author
self.game_status[ctx.channel.id] = True
start_embed = self.make_start_embed(category)
@@ -85,11 +292,10 @@ class TriviaQuiz(commands.Cog):
await ctx.send(embed=start_embed) # send an embed with the rules
await asyncio.sleep(1)
- topic = self.questions[category]
-
done_question = []
hint_no = 0
- answer = None
+ answers = None
+
while self.game_status[ctx.channel.id]:
# Exit quiz if number of questions for a round are already sent.
if len(done_question) > self.question_limit and hint_no == 0:
@@ -111,34 +317,58 @@ class TriviaQuiz(commands.Cog):
done_question.append(question_dict["id"])
break
- q = question_dict["question"]
- answer = question_dict["answer"]
+ if "dynamic_id" not in question_dict:
+ question = question_dict["question"]
+ answers = question_dict["answer"].split(", ")
+
+ var_tol = STANDARD_VARIATION_TOLERANCE
+ else:
+ format_func = DYNAMIC_QUESTIONS_FORMAT_FUNCS[question_dict["dynamic_id"]]
+
+ quiz_entry = format_func(
+ question_dict["question"],
+ question_dict["answer"],
+ )
- embed = discord.Embed(colour=discord.Colour.gold())
- embed.title = f"Question #{len(done_question)}"
- embed.description = q
- await ctx.send(embed=embed) # Send question embed.
+ question, answers = quiz_entry.question, quiz_entry.answer
+ answers = [answers]
- # A function to check whether user input is the correct answer(close to the right answer)
- def check(m: discord.Message) -> bool:
- return (
- m.channel == ctx.channel
- and fuzz.ratio(answer.lower(), m.content.lower()) > 85
+ var_tol = DYNAMICALLY_GEN_VARIATION_TOLERANCE
+
+ embed = discord.Embed(
+ colour=Colours.gold,
+ title=f"Question #{len(done_question)}",
+ description=question,
)
+ if img_url := question_dict.get("img_url"):
+ embed.set_image(url=img_url)
+
+ await ctx.send(embed=embed)
+
+ def check_func(variation_tolerance: int) -> Callable[[discord.Message], bool]:
+ def contains_correct_answer(m: discord.Message) -> bool:
+ return m.channel == ctx.channel and any(
+ fuzz.ratio(answer.lower(), m.content.lower()) > variation_tolerance
+ for answer in answers
+ )
+
+ return contains_correct_answer
+
try:
- msg = await self.bot.wait_for('message', check=check, timeout=10)
+ msg = await self.bot.wait_for("message", check=check_func(var_tol), timeout=10)
except asyncio.TimeoutError:
# In case of TimeoutError and the game has been stopped, then do nothing.
- if self.game_status[ctx.channel.id] is False:
+ if not self.game_status[ctx.channel.id]:
break
- # if number of hints sent or time alerts sent is less than 2, then send one.
if hint_no < 2:
hint_no += 1
+
if "hints" in question_dict:
hints = question_dict["hints"]
- await ctx.send(f"**Hint #{hint_no+1}\n**{hints[hint_no]}")
+
+ await ctx.send(f"**Hint #{hint_no}\n**{hints[hint_no - 1]}")
else:
await ctx.send(f"{30 - hint_no * 10}s left!")
@@ -151,10 +381,17 @@ class TriviaQuiz(commands.Cog):
response = random.choice(WRONG_ANS_RESPONSE)
await ctx.send(response)
- await self.send_answer(ctx.channel, question_dict)
+
+ await self.send_answer(
+ ctx.channel,
+ answers,
+ False,
+ question_dict,
+ self.question_limit - len(done_question) + 1,
+ )
await asyncio.sleep(1)
- hint_no = 0 # init hint_no = 0 so that 2 hints/time alerts can be sent for the new question.
+ hint_no = 0 # Reset the hint counter so that on the next round, it's in the initial state
await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
await asyncio.sleep(2)
@@ -162,8 +399,7 @@ class TriviaQuiz(commands.Cog):
if self.game_status[ctx.channel.id] is False:
break
- # Reduce points by 25 for every hint/time alert that has been sent.
- points = 100 - 25*hint_no
+ points = 100 - 25 * hint_no
if msg.author in self.game_player_scores[ctx.channel.id]:
self.game_player_scores[ctx.channel.id][msg.author] += points
else:
@@ -178,23 +414,50 @@ class TriviaQuiz(commands.Cog):
hint_no = 0
await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!")
- await self.send_answer(ctx.channel, question_dict)
+
+ await self.send_answer(
+ ctx.channel,
+ answers,
+ True,
+ question_dict,
+ self.question_limit - len(done_question) + 1,
+ )
await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
+
await asyncio.sleep(2)
- @staticmethod
- def make_start_embed(category: str) -> discord.Embed:
+ def make_start_embed(self, category: str) -> discord.Embed:
"""Generate a starting/introduction embed for the quiz."""
- start_embed = discord.Embed(colour=discord.Colour.red())
- start_embed.title = "Quiz game Starting!!"
- start_embed.description = "Each game consists of 5 questions.\n"
- start_embed.description += "**Rules :**\nNo cheating and have fun!"
- start_embed.description += f"\n **Category** : {category}"
+ start_embed = discord.Embed(
+ colour=Colours.blue,
+ title="Quiz game starting!",
+ description=(
+ f"This game consists of {self.question_limit + 1} questions.\n"
+ "**Rules: **No cheating and have fun!\n"
+ f"**Category**: {category}"
+ ),
+ )
+
start_embed.set_footer(
- text="Points for each question reduces by 25 after 10s or after a hint. Total time is 30s per question"
+ text=(
+ "Points for each question reduces by 25 after 10s or after a hint. "
+ "Total time is 30s per question"
+ )
)
+
return start_embed
+ @staticmethod
+ def make_error_embed(desc: str) -> discord.Embed:
+ """Generate an error embed with the given description."""
+ error_embed = discord.Embed(
+ colour=Colours.soft_red,
+ title=random.choice(NEGATIVE_REPLIES),
+ description=desc,
+ )
+
+ return error_embed
+
@quiz_game.command(name="stop")
async def stop_quiz(self, ctx: commands.Context) -> None:
"""
@@ -204,16 +467,17 @@ class TriviaQuiz(commands.Cog):
"""
if self.game_status[ctx.channel.id] is True:
# Check if the author is the game starter or a moderator.
- if (
- ctx.author == self.game_owners[ctx.channel.id]
- or any(Roles.moderator == role.id for role in ctx.author.roles)
+ if ctx.author == self.game_owners[ctx.channel.id] or any(
+ Roles.moderator == role.id for role in ctx.author.roles
):
+
await ctx.send("Quiz stopped.")
await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
self.game_status[ctx.channel.id] = False
del self.game_owners[ctx.channel.id]
self.game_player_scores[ctx.channel.id] = {}
+
else:
await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!")
else:
@@ -226,18 +490,20 @@ class TriviaQuiz(commands.Cog):
@staticmethod
async def send_score(channel: discord.TextChannel, player_data: dict) -> None:
- """A function which sends the score."""
+ """Send the current scores of players in the game channel."""
if len(player_data) == 0:
await channel.send("No one has made it onto the leaderboard yet.")
return
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "Score Board"
- embed.description = ""
+ embed = discord.Embed(
+ colour=Colours.blue,
+ title="Score Board",
+ description="",
+ )
- sorted_dict = sorted(player_data.items(), key=lambda a: a[1], reverse=True)
+ sorted_dict = sorted(player_data.items(), key=operator.itemgetter(1), reverse=True)
for item in sorted_dict:
- embed.description += f"{item[0]} : {item[1]}\n"
+ embed.description += f"{item[0]}: {item[1]}\n"
await channel.send(embed=embed)
@@ -250,7 +516,6 @@ class TriviaQuiz(commands.Cog):
# Check if more than 1 player has highest points.
if no_of_winners > 1:
- word = "You guys"
winners = []
points_copy = list(player_data.values()).copy()
@@ -261,44 +526,65 @@ class TriviaQuiz(commands.Cog):
winners_mention = " ".join(winner.mention for winner in winners)
else:
- word = "You"
author_index = list(player_data.values()).index(highest_points)
winner = list(player_data.keys())[author_index]
winners_mention = winner.mention
await channel.send(
f"Congratulations {winners_mention} :tada: "
- f"{word} have won this quiz game with a grand total of {highest_points} points!"
+ f"You have won this quiz game with a grand total of {highest_points} points!"
)
def category_embed(self) -> discord.Embed:
"""Build an embed showing all available trivia categories."""
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "The available question categories are:"
+ embed = discord.Embed(
+ colour=Colours.blue,
+ title="The available question categories are:",
+ description="",
+ )
+
embed.set_footer(text="If a category is not chosen, a random one will be selected.")
- embed.description = ""
for cat, description in self.categories.items():
- embed.description += f"**- {cat.capitalize()}**\n{description.capitalize()}\n"
+ embed.description += (
+ f"**- {cat.capitalize()}**\n"
+ f"{description.capitalize()}\n"
+ )
return embed
@staticmethod
- async def send_answer(channel: discord.TextChannel, question_dict: dict) -> None:
+ async def send_answer(
+ channel: discord.TextChannel,
+ answers: List[str],
+ answer_is_correct: bool,
+ question_dict: dict,
+ q_left: int,
+ ) -> None:
"""Send the correct answer of a question to the game channel."""
- answer = question_dict["answer"]
- info = question_dict["info"]
- embed = discord.Embed(color=discord.Colour.red())
- embed.title = f"The correct answer is **{answer}**\n"
- embed.description = ""
+ info = question_dict.get("info")
+
+ plurality = " is" if len(answers) == 1 else "s are"
- if info != "":
+ embed = discord.Embed(
+ color=Colours.bright_green,
+ title=(
+ ("You got it! " if answer_is_correct else "")
+ + f"The correct answer{plurality} **`{', '.join(answers)}`**\n"
+ ),
+ description="",
+ )
+
+ if info is not None:
embed.description += f"**Information**\n{info}\n\n"
- embed.description += "Let's move to the next question.\nRemaining questions: "
+ embed.description += (
+ ("Let's move to the next question." if q_left > 0 else "")
+ + f"\nRemaining questions: {q_left}"
+ )
await channel.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Load the cog."""
+def setup(bot: Bot) -> None:
+ """Load the TriviaQuiz cog."""
bot.add_cog(TriviaQuiz(bot))
diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py
index 068c4f43..83937438 100644
--- a/bot/exts/evergreen/wikipedia.py
+++ b/bot/exts/evergreen/wikipedia.py
@@ -20,7 +20,7 @@ WIKI_THUMBNAIL = (
"https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg"
"/330px-Wikipedia-logo-v2.svg.png"
)
-WIKI_SNIPPET_REGEX = r'(<!--.*?-->|<[^>]*>)'
+WIKI_SNIPPET_REGEX = r"(<!--.*?-->|<[^>]*>)"
WIKI_SEARCH_RESULT = (
"**[{name}]({url})**\n"
"{description}\n"
@@ -39,18 +39,18 @@ class WikipediaSearch(commands.Cog):
async with self.bot.http_session.get(url=url) as resp:
if resp.status == 200:
raw_data = await resp.json()
- number_of_results = raw_data['query']['searchinfo']['totalhits']
+ number_of_results = raw_data["query"]["searchinfo"]["totalhits"]
if number_of_results:
- results = raw_data['query']['search']
+ results = raw_data["query"]["search"]
lines = []
for article in results:
line = WIKI_SEARCH_RESULT.format(
- name=article['title'],
+ name=article["title"],
description=unescape(
re.sub(
- WIKI_SNIPPET_REGEX, '', article['snippet']
+ WIKI_SNIPPET_REGEX, "", article["snippet"]
)
),
url=f"https://en.wikipedia.org/?curid={article['pageid']}"
@@ -72,7 +72,7 @@ class WikipediaSearch(commands.Cog):
return
@commands.cooldown(1, 10, commands.BucketType.user)
- @commands.command(name="wikipedia", aliases=["wiki"])
+ @commands.command(name="wikipedia", aliases=("wiki",))
async def wikipedia_search_command(self, ctx: commands.Context, *, search: str) -> None:
"""Sends paginated top 10 results of Wikipedia search.."""
contents = await self.wiki_request(ctx.channel, search)
@@ -90,5 +90,5 @@ class WikipediaSearch(commands.Cog):
def setup(bot: Bot) -> None:
- """Wikipedia Cog load."""
+ """Load the WikipediaSearch cog."""
bot.add_cog(WikipediaSearch(bot))
diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py
index 14ec1041..d23afd6f 100644
--- a/bot/exts/evergreen/wolfram.py
+++ b/bot/exts/evergreen/wolfram.py
@@ -9,6 +9,7 @@ from discord import Embed
from discord.ext import commands
from discord.ext.commands import BucketType, Cog, Context, check, group
+from bot.bot import Bot
from bot.constants import Colours, STAFF_ROLES, Wolfram
from bot.utils.pagination import ImagePaginator
@@ -39,9 +40,11 @@ async def send_embed(
"""Generate & send a response embed with Wolfram as the author."""
embed = Embed(colour=colour)
embed.description = message_txt
- embed.set_author(name="Wolfram Alpha",
- icon_url=WOLF_IMAGE,
- url="https://www.wolframalpha.com/")
+ embed.set_author(
+ name="Wolfram Alpha",
+ icon_url=WOLF_IMAGE,
+ url="https://www.wolframalpha.com/"
+ )
if footer:
embed.set_footer(text=footer)
@@ -55,10 +58,10 @@ def custom_cooldown(*ignore: List[int]) -> Callable:
"""
Implement per-user and per-guild cooldowns for requests to the Wolfram API.
- A list of roles may be provided to ignore the per-user cooldown
+ A list of roles may be provided to ignore the per-user cooldown.
"""
async def predicate(ctx: Context) -> bool:
- if ctx.invoked_with == 'help':
+ if ctx.invoked_with == "help":
# if the invoked command is help we don't want to increase the ratelimits since it's not actually
# invoking the command/making a request, so instead just check if the user/guild are on cooldown.
guild_cooldown = not guildcd.get_bucket(ctx.message).get_tokens() == 0 # if guild is on cooldown
@@ -102,9 +105,9 @@ def custom_cooldown(*ignore: List[int]) -> Callable:
return check(predicate)
-async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional[List[Tuple]]:
+async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[List[Tuple]]:
"""Get the Wolfram API pod pages for the provided query."""
- async with ctx.channel.typing():
+ async with ctx.typing():
url_str = parse.urlencode({
"input": query,
"appid": APPID,
@@ -117,7 +120,7 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional
request_url = QUERY.format(request="query", data=url_str)
async with bot.http_session.get(request_url) as response:
- json = await response.json(content_type='text/plain')
+ json = await response.json(content_type="text/plain")
result = json["queryresult"]
@@ -162,7 +165,7 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional
class Wolfram(Cog):
"""Commands for interacting with the Wolfram|Alpha API."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@group(name="wolfram", aliases=("wolf", "wa"), invoke_without_command=True)
@@ -179,7 +182,7 @@ class Wolfram(Cog):
query = QUERY.format(request="simple", data=url_str)
# Give feedback that the bot is working.
- async with ctx.channel.typing():
+ async with ctx.typing():
async with self.bot.http_session.get(query) as response:
status = response.status
image_bytes = await response.read()
@@ -188,11 +191,11 @@ class Wolfram(Cog):
image_url = "attachment://image.png"
if status == 501:
- message = "Failed to get response"
+ message = "Failed to get response."
footer = ""
color = Colours.soft_red
elif status == 400:
- message = "No input found"
+ message = "No input found."
footer = ""
color = Colours.soft_red
elif status == 403:
@@ -221,9 +224,11 @@ class Wolfram(Cog):
return
embed = Embed()
- embed.set_author(name="Wolfram Alpha",
- icon_url=WOLF_IMAGE,
- url="https://www.wolframalpha.com/")
+ embed.set_author(
+ name="Wolfram Alpha",
+ icon_url=WOLF_IMAGE,
+ url="https://www.wolframalpha.com/"
+ )
embed.colour = Colours.soft_orange
await ImagePaginator.paginate(pages, ctx, embed)
@@ -262,18 +267,18 @@ class Wolfram(Cog):
query = QUERY.format(request="result", data=url_str)
# Give feedback that the bot is working.
- async with ctx.channel.typing():
+ async with ctx.typing():
async with self.bot.http_session.get(query) as response:
status = response.status
response_text = await response.text()
if status == 501:
- message = "Failed to get response"
+ message = "Failed to get response."
color = Colours.soft_red
elif status == 400:
- message = "No input found"
+ message = "No input found."
color = Colours.soft_red
- elif response_text == "Error 1: Invalid appid":
+ elif response_text == "Error 1: Invalid appid.":
message = "Wolfram API key is invalid or missing."
color = Colours.soft_red
else:
@@ -283,6 +288,6 @@ class Wolfram(Cog):
await send_embed(ctx, message, color)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Wolfram cog."""
bot.add_cog(Wolfram(bot))
diff --git a/bot/exts/evergreen/wonder_twins.py b/bot/exts/evergreen/wonder_twins.py
index afc5346e..40edf785 100644
--- a/bot/exts/evergreen/wonder_twins.py
+++ b/bot/exts/evergreen/wonder_twins.py
@@ -2,15 +2,15 @@ import random
from pathlib import Path
import yaml
-from discord.ext.commands import Bot, Cog, Context, command
+from discord.ext.commands import Cog, Context, command
+
+from bot.bot import Bot
class WonderTwins(Cog):
"""Cog for a Wonder Twins inspired command."""
- def __init__(self, bot: Bot):
- self.bot = bot
-
+ def __init__(self):
with open(Path.cwd() / "bot" / "resources" / "evergreen" / "wonder_twins.yaml", "r", encoding="utf-8") as f:
info = yaml.load(f, Loader=yaml.FullLoader)
self.water_types = info["water_types"]
@@ -38,7 +38,7 @@ class WonderTwins(Cog):
object_name = self.append_onto(adjective, object_name)
return f"{object_name} of {water_type}"
- @command(name="formof", aliases=["wondertwins", "wondertwin", "fo"])
+ @command(name="formof", aliases=("wondertwins", "wondertwin", "fo"))
async def form_of(self, ctx: Context) -> None:
"""Command to send a Wonder Twins inspired phrase to the user invoking the command."""
await ctx.send(f"Form of {self.format_phrase()}!")
@@ -46,4 +46,4 @@ class WonderTwins(Cog):
def setup(bot: Bot) -> None:
"""Load the WonderTwins cog."""
- bot.add_cog(WonderTwins(bot))
+ bot.add_cog(WonderTwins())
diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py
index 1ff98ca2..c98830bc 100644
--- a/bot/exts/evergreen/xkcd.py
+++ b/bot/exts/evergreen/xkcd.py
@@ -53,7 +53,7 @@ class XKCD(Cog):
await ctx.send(embed=embed)
return
- comic = randint(1, self.latest_comic_info['num']) if comic is None else comic.group(0)
+ comic = randint(1, self.latest_comic_info["num"]) if comic is None else comic.group(0)
if comic == "latest":
info = self.latest_comic_info
@@ -69,7 +69,7 @@ class XKCD(Cog):
return
embed.title = f"XKCD comic #{info['num']}"
- embed.description = info['alt']
+ embed.description = info["alt"]
embed.url = f"{BASE_URL}/{info['num']}"
if info["img"][-3:] in ("jpg", "png", "gif"):
@@ -87,5 +87,5 @@ class XKCD(Cog):
def setup(bot: Bot) -> None:
- """Loading the XKCD cog."""
+ """Load the XKCD cog."""
bot.add_cog(XKCD(bot))
diff --git a/bot/exts/halloween/8ball.py b/bot/exts/halloween/8ball.py
index 1df48fbf..a2431190 100644
--- a/bot/exts/halloween/8ball.py
+++ b/bot/exts/halloween/8ball.py
@@ -6,28 +6,26 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with open(Path("bot/resources/halloween/responses.json"), "r", encoding="utf8") as f:
- responses = json.load(f)
+RESPONSES = json.loads(Path("bot/resources/halloween/responses.json").read_text("utf8"))
class SpookyEightBall(commands.Cog):
"""Spooky Eightball answers."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('spooky8ball',))
+ @commands.command(aliases=("spooky8ball",))
async def spookyeightball(self, ctx: commands.Context, *, question: str) -> None:
"""Responds with a random response to a question."""
- choice = random.choice(responses['responses'])
+ choice = random.choice(RESPONSES["responses"])
msg = await ctx.send(choice[0])
if len(choice) > 1:
await asyncio.sleep(random.randint(2, 5))
await msg.edit(content=f"{choice[0]} \n{choice[1]}")
-def setup(bot: commands.Bot) -> None:
- """Spooky Eight Ball Cog Load."""
- bot.add_cog(SpookyEightBall(bot))
+def setup(bot: Bot) -> None:
+ """Load the Spooky Eight Ball Cog."""
+ bot.add_cog(SpookyEightBall())
diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py
index 40e21f40..4afd5913 100644
--- a/bot/exts/halloween/candy_collection.py
+++ b/bot/exts/halloween/candy_collection.py
@@ -6,6 +6,7 @@ import discord
from async_rediscache import RedisCache
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Channels, Month
from bot.utils.decorators import in_month
@@ -21,11 +22,11 @@ EMOJIS = dict(
CANDY="\N{CANDY}",
SKULL="\N{SKULL}",
MEDALS=(
- '\N{FIRST PLACE MEDAL}',
- '\N{SECOND PLACE MEDAL}',
- '\N{THIRD PLACE MEDAL}',
- '\N{SPORTS MEDAL}',
- '\N{SPORTS MEDAL}',
+ "\N{FIRST PLACE MEDAL}",
+ "\N{SECOND PLACE MEDAL}",
+ "\N{THIRD PLACE MEDAL}",
+ "\N{SPORTS MEDAL}",
+ "\N{SPORTS MEDAL}",
),
)
@@ -40,7 +41,7 @@ class CandyCollection(commands.Cog):
candy_messages = RedisCache()
skull_messages = RedisCache()
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@in_month(Month.OCTOBER)
@@ -60,15 +61,15 @@ class CandyCollection(commands.Cog):
# do random check for skull first as it has the lower chance
if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:
await self.skull_messages.set(message.id, "skull")
- return await message.add_reaction(EMOJIS['SKULL'])
+ await message.add_reaction(EMOJIS["SKULL"])
# check for the candy chance next
- if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
+ elif random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
await self.candy_messages.set(message.id, "candy")
- return await message.add_reaction(EMOJIS['CANDY'])
+ await message.add_reaction(EMOJIS["CANDY"])
@in_month(Month.OCTOBER)
@commands.Cog.listener()
- async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member) -> None:
+ async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.User, discord.Member]) -> None:
"""Add/remove candies from a person if the reaction satisfies criteria."""
message = reaction.message
# check to ensure the reactor is human
@@ -81,7 +82,7 @@ class CandyCollection(commands.Cog):
# if its not a candy or skull, and it is one of 10 most recent messages,
# proceed to add a skull/candy with higher chance
- if str(reaction.emoji) not in (EMOJIS['SKULL'], EMOJIS['CANDY']):
+ if str(reaction.emoji) not in (EMOJIS["SKULL"], EMOJIS["CANDY"]):
recent_message_ids = map(
lambda m: m.id,
await self.hacktober_channel.history(limit=10).flatten()
@@ -90,14 +91,14 @@ class CandyCollection(commands.Cog):
await self.reacted_msg_chance(message)
return
- if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS['CANDY']:
+ if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS["CANDY"]:
await self.candy_messages.delete(message.id)
if await self.candy_records.contains(user.id):
await self.candy_records.increment(user.id)
else:
await self.candy_records.set(user.id, 1)
- elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS['SKULL']:
+ elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS["SKULL"]:
await self.skull_messages.delete(message.id)
if prev_record := await self.candy_records.get(user.id):
@@ -105,7 +106,7 @@ class CandyCollection(commands.Cog):
await self.candy_records.decrement(user.id, lost)
if lost == prev_record:
- await CandyCollection.send_spook_msg(user, message.channel, 'all of your')
+ await CandyCollection.send_spook_msg(user, message.channel, "all of your")
else:
await CandyCollection.send_spook_msg(user, message.channel, lost)
else:
@@ -124,11 +125,11 @@ class CandyCollection(commands.Cog):
"""
if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:
await self.skull_messages.set(message.id, "skull")
- return await message.add_reaction(EMOJIS['SKULL'])
+ await message.add_reaction(EMOJIS["SKULL"])
- if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
+ elif random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
await self.candy_messages.set(message.id, "candy")
- return await message.add_reaction(EMOJIS['CANDY'])
+ await message.add_reaction(EMOJIS["CANDY"])
@property
def hacktober_channel(self) -> discord.TextChannel:
@@ -141,8 +142,10 @@ class CandyCollection(commands.Cog):
) -> None:
"""Send a spooky message."""
e = discord.Embed(colour=author.colour)
- e.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
- f"I took {candies} candies and quickly took flight.")
+ e.set_author(
+ name="Ghosts and Ghouls and Jack o' lanterns at night; "
+ f"I took {candies} candies and quickly took flight."
+ )
await channel.send(embed=e)
@staticmethod
@@ -152,8 +155,12 @@ class CandyCollection(commands.Cog):
) -> None:
"""An alternative spooky message sent when user has no candies in the collection."""
embed = discord.Embed(color=author.color)
- embed.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
- "I tried to take your candies but you had none to begin with!")
+ embed.set_author(
+ name=(
+ "Ghosts and Ghouls and Jack o' lanterns at night; "
+ "I tried to take your candies but you had none to begin with!"
+ )
+ )
await channel.send(embed=embed)
@in_month(Month.OCTOBER)
@@ -170,10 +177,10 @@ class CandyCollection(commands.Cog):
)
top_five = top_sorted[:5]
- return '\n'.join(
+ return "\n".join(
f"{EMOJIS['MEDALS'][index]} <@{record[0]}>: {record[1]}"
for index, record in enumerate(top_five)
- ) if top_five else 'No Candies'
+ ) if top_five else "No Candies"
e = discord.Embed(colour=discord.Colour.blurple())
e.add_field(
@@ -182,7 +189,7 @@ class CandyCollection(commands.Cog):
inline=False
)
e.add_field(
- name='\u200b',
+ name="\u200b",
value="Candies will randomly appear on messages sent. "
"\nHit the candy when it appears as fast as possible to get the candy! "
"\nBut beware the ghosts...",
@@ -191,6 +198,6 @@ class CandyCollection(commands.Cog):
await ctx.send(embed=e)
-def setup(bot: commands.Bot) -> None:
- """Candy Collection game Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Candy Collection Cog."""
bot.add_cog(CandyCollection(bot))
diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py
index 9deadde9..20a06770 100644
--- a/bot/exts/halloween/hacktober-issue-finder.py
+++ b/bot/exts/halloween/hacktober-issue-finder.py
@@ -3,10 +3,10 @@ import logging
import random
from typing import Dict, Optional
-import aiohttp
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Month, Tokens
from bot.utils.decorators import in_month
@@ -25,7 +25,7 @@ if GITHUB_TOKEN := Tokens.github:
class HacktoberIssues(commands.Cog):
"""Find a random hacktober python issue on GitHub."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.cache_normal = None
self.cache_timer_normal = datetime.datetime(1, 1, 1)
@@ -41,7 +41,7 @@ class HacktoberIssues(commands.Cog):
If the command is run with beginner (`.hacktoberissues beginner`):
It will also narrow it down to the "first good issue" label.
"""
- with ctx.typing():
+ async with ctx.typing():
issues = await self.get_issues(ctx, option)
if issues is None:
return
@@ -59,40 +59,41 @@ class HacktoberIssues(commands.Cog):
log.debug("using cache")
return self.cache_normal
- async with aiohttp.ClientSession() as session:
+ if option == "beginner":
+ url = URL + '+label:"good first issue"'
+ if self.cache_beginner is not None:
+ page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100)
+ url += f"&page={page}"
+ else:
+ url = URL
+ if self.cache_normal is not None:
+ page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100)
+ url += f"&page={page}"
+
+ log.debug(f"making api request to url: {url}")
+ async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as response:
+ if response.status != 200:
+ log.error(f"expected 200 status (got {response.status}) by the GitHub api.")
+ await ctx.send(
+ f"ERROR: expected 200 status (got {response.status}) by the GitHub api.\n"
+ f"{await response.text()}"
+ )
+ return None
+ data = await response.json()
+
+ if len(data["items"]) == 0:
+ log.error(f"no issues returned by GitHub API, with url: {response.url}")
+ await ctx.send(f"ERROR: no issues returned by GitHub API, with url: {response.url}")
+ return None
+
if option == "beginner":
- url = URL + '+label:"good first issue"'
- if self.cache_beginner is not None:
- page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100)
- url += f"&page={page}"
+ self.cache_beginner = data
+ self.cache_timer_beginner = ctx.message.created_at
else:
- url = URL
- if self.cache_normal is not None:
- page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100)
- url += f"&page={page}"
-
- log.debug(f"making api request to url: {url}")
- async with session.get(url, headers=REQUEST_HEADERS) as response:
- if response.status != 200:
- log.error(f"expected 200 status (got {response.status}) from the GitHub api.")
- await ctx.send(f"ERROR: expected 200 status (got {response.status}) from the GitHub api.")
- await ctx.send(await response.text())
- return None
- data = await response.json()
-
- if len(data["items"]) == 0:
- log.error(f"no issues returned from GitHub api. with url: {response.url}")
- await ctx.send(f"ERROR: no issues returned from GitHub api. with url: {response.url}")
- return None
-
- if option == "beginner":
- self.cache_beginner = data
- self.cache_timer_beginner = ctx.message.created_at
- else:
- self.cache_normal = data
- self.cache_timer_normal = ctx.message.created_at
-
- return data
+ self.cache_normal = data
+ self.cache_timer_normal = ctx.message.created_at
+
+ return data
@staticmethod
def format_embed(issue: Dict) -> discord.Embed:
@@ -103,7 +104,7 @@ class HacktoberIssues(commands.Cog):
labels = [label["name"] for label in issue["labels"]]
embed = discord.Embed(title=title)
- embed.description = body[:500] + '...' if len(body) > 500 else body
+ embed.description = body[:500] + "..." if len(body) > 500 else body
embed.add_field(name="labels", value="\n".join(labels))
embed.url = issue_url
embed.set_footer(text=issue_url)
@@ -111,6 +112,6 @@ class HacktoberIssues(commands.Cog):
return embed
-def setup(bot: commands.Bot) -> None:
- """Hacktober issue finder Cog Load."""
+def setup(bot: Bot) -> None:
+ """Load the HacktoberIssue finder."""
bot.add_cog(HacktoberIssues(bot))
diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py
index d9fc0e8a..b74e680b 100644
--- a/bot/exts/halloween/hacktoberstats.py
+++ b/bot/exts/halloween/hacktoberstats.py
@@ -5,12 +5,12 @@ from collections import Counter
from datetime import datetime, timedelta
from typing import List, Optional, Tuple, Union
-import aiohttp
import discord
from async_rediscache import RedisCache
from discord.ext import commands
-from bot.constants import Channels, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS
+from bot.bot import Bot
+from bot.constants import Channels, Colours, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS
from bot.utils.decorators import in_month, whitelist_override
log = logging.getLogger(__name__)
@@ -39,7 +39,7 @@ class HacktoberStats(commands.Cog):
# Stores mapping of user IDs and GitHub usernames
linked_accounts = RedisCache()
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@@ -83,15 +83,15 @@ class HacktoberStats(commands.Cog):
if github_username:
if await self.linked_accounts.contains(author_id):
old_username = await self.linked_accounts.get(author_id)
- logging.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
+ log.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
await ctx.send(f"{author_mention}, your GitHub username has been updated to: '{github_username}'")
else:
- logging.info(f"{author_id} has added a github link to '{github_username}'")
+ log.info(f"{author_id} has added a github link to '{github_username}'")
await ctx.send(f"{author_mention}, your GitHub username has been added")
await self.linked_accounts.set(author_id, github_username)
else:
- logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
+ log.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
await ctx.send(f"{author_mention}, a GitHub username is required to link your account")
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@@ -138,7 +138,7 @@ class HacktoberStats(commands.Cog):
if prs:
stats_embed = await self.build_embed(github_username, prs)
- await ctx.send('Here are some stats!', embed=stats_embed)
+ await ctx.send("Here are some stats!", embed=stats_embed)
else:
await ctx.send(f"No valid Hacktoberfest PRs found for '{github_username}'")
@@ -157,7 +157,7 @@ class HacktoberStats(commands.Cog):
stats_embed = discord.Embed(
title=f"{github_username}'s Hacktoberfest",
- color=discord.Color(0x9c4af7),
+ color=Colours.purple,
description=(
f"{github_username} has made {n} valid "
f"{self._contributionator(n)} in "
@@ -188,8 +188,7 @@ class HacktoberStats(commands.Cog):
logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")
return stats_embed
- @staticmethod
- async def get_october_prs(github_username: str) -> Optional[List[dict]]:
+ async def get_october_prs(self, github_username: str) -> Optional[List[dict]]:
"""
Query GitHub's API for PRs created during the month of October by github_username.
@@ -212,7 +211,7 @@ class HacktoberStats(commands.Cog):
Otherwise, return empty list.
None will be returned when the GitHub user was not found.
"""
- logging.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'")
+ log.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'")
base_url = "https://api.github.com/search/issues?q="
action_type = "pr"
is_query = "public"
@@ -228,24 +227,24 @@ class HacktoberStats(commands.Cog):
f"+created:{date_range}"
f"&per_page={per_page}"
)
- logging.debug(f"GitHub query URL generated: {query_url}")
+ log.debug(f"GitHub query URL generated: {query_url}")
- jsonresp = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS)
- if "message" in jsonresp.keys():
+ jsonresp = await self._fetch_url(query_url, REQUEST_HEADERS)
+ if "message" in jsonresp:
# One of the parameters is invalid, short circuit for now
api_message = jsonresp["errors"][0]["message"]
# Ignore logging non-existent users or users we do not have permission to see
if api_message == GITHUB_NONEXISTENT_USER_MESSAGE:
- logging.debug(f"No GitHub user found named '{github_username}'")
+ log.debug(f"No GitHub user found named '{github_username}'")
return
else:
- logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
+ log.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
return [] # No October PRs were found due to error
if jsonresp["total_count"] == 0:
# Short circuit if there aren't any PRs
- logging.info(f"No October PRs found for GitHub user: '{github_username}'")
+ log.info(f"No October PRs found for GitHub user: '{github_username}'")
return []
logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'")
@@ -253,20 +252,20 @@ class HacktoberStats(commands.Cog):
oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=None)
hackto_topics = {} # cache whether each repo has the appropriate topic (bool values)
for item in jsonresp["items"]:
- shortname = HacktoberStats._get_shortname(item["repository_url"])
+ shortname = self._get_shortname(item["repository_url"])
itemdict = {
"repo_url": f"https://www.github.com/{shortname}",
"repo_shortname": shortname,
"created_at": datetime.strptime(
- item["created_at"], r"%Y-%m-%dT%H:%M:%SZ"
+ item["created_at"], "%Y-%m-%dT%H:%M:%SZ"
),
"number": item["number"]
}
# If the PR has 'invalid' or 'spam' labels, the PR must be
# either merged or approved for it to be included
- if HacktoberStats._has_label(item, ["invalid", "spam"]):
- if not await HacktoberStats._is_accepted(itemdict):
+ if self._has_label(item, ["invalid", "spam"]):
+ if not await self._is_accepted(itemdict):
continue
# PRs before oct 3 no need to check for topics
@@ -277,21 +276,20 @@ class HacktoberStats(commands.Cog):
continue
# Checking PR's labels for "hacktoberfest-accepted"
- if HacktoberStats._has_label(item, "hacktoberfest-accepted"):
+ if self._has_label(item, "hacktoberfest-accepted"):
outlist.append(itemdict)
continue
# No need to query GitHub if repo topics are fetched before already
- if shortname in hackto_topics.keys():
- if hackto_topics[shortname]:
- outlist.append(itemdict)
- continue
+ if hackto_topics.get(shortname):
+ outlist.append(itemdict)
+ continue
# Fetch topics for the PR's repo
topics_query_url = f"https://api.github.com/repos/{shortname}/topics"
- logging.debug(f"Fetching repo topics for {shortname} with url: {topics_query_url}")
- jsonresp2 = await HacktoberStats._fetch_url(topics_query_url, GITHUB_TOPICS_ACCEPT_HEADER)
+ log.debug(f"Fetching repo topics for {shortname} with url: {topics_query_url}")
+ jsonresp2 = await self._fetch_url(topics_query_url, GITHUB_TOPICS_ACCEPT_HEADER)
if jsonresp2.get("names") is None:
- logging.error(f"Error fetching topics for {shortname}: {jsonresp2['message']}")
+ log.error(f"Error fetching topics for {shortname}: {jsonresp2['message']}")
continue # Assume the repo doesn't have the `hacktoberfest` topic if API request errored
# PRs after oct 3 that doesn't have 'hacktoberfest-accepted' label
@@ -301,13 +299,10 @@ class HacktoberStats(commands.Cog):
outlist.append(itemdict)
return outlist
- @staticmethod
- async def _fetch_url(url: str, headers: dict) -> dict:
+ async def _fetch_url(self, url: str, headers: dict) -> dict:
"""Retrieve API response from URL."""
- async with aiohttp.ClientSession() as session:
- async with session.get(url, headers=headers) as resp:
- jsonresp = await resp.json()
- return jsonresp
+ async with self.bot.http_session.get(url, headers=headers) as resp:
+ return await resp.json()
@staticmethod
def _has_label(pr: dict, labels: Union[List[str], str]) -> bool:
@@ -319,40 +314,36 @@ class HacktoberStats(commands.Cog):
"""
if not pr.get("labels"): # if PR has no labels
return False
- if (isinstance(labels, str)) and (any(label["name"].casefold() == labels for label in pr["labels"])):
+ if isinstance(labels, str) and any(label["name"].casefold() == labels for label in pr["labels"]):
return True
for item in labels:
if any(label["name"].casefold() == item for label in pr["labels"]):
return True
return False
- @staticmethod
- async def _is_accepted(pr: dict) -> bool:
+ async def _is_accepted(self, pr: dict) -> bool:
"""Check if a PR is merged, approved, or labelled hacktoberfest-accepted."""
# checking for merge status
- query_url = f"https://api.github.com/repos/{pr['repo_shortname']}/pulls/"
- query_url += str(pr["number"])
- jsonresp = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS)
-
- if "message" in jsonresp.keys():
- logging.error(
- f"Error fetching PR stats for #{pr['number']} in repo {pr['repo_shortname']}:\n"
- f"{jsonresp['message']}"
- )
+ query_url = f"https://api.github.com/repos/{pr['repo_shortname']}/pulls/{pr['number']}"
+ jsonresp = await self._fetch_url(query_url, REQUEST_HEADERS)
+
+ if message := jsonresp.get("message"):
+ log.error(f"Error fetching PR stats for #{pr['number']} in repo {pr['repo_shortname']}:\n{message}")
return False
- if ("merged" in jsonresp.keys()) and jsonresp["merged"]:
+
+ if jsonresp.get("merged"):
return True
# checking for the label, using `jsonresp` which has the label information
- if HacktoberStats._has_label(jsonresp, "hacktoberfest-accepted"):
+ if self._has_label(jsonresp, "hacktoberfest-accepted"):
return True
# checking approval
query_url += "/reviews"
- jsonresp2 = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS)
+ jsonresp2 = await self._fetch_url(query_url, REQUEST_HEADERS)
if isinstance(jsonresp2, dict):
# if API request is unsuccessful it will be a dict with the error in 'message'
- logging.error(
+ log.error(
f"Error fetching PR reviews for #{pr['number']} in repo {pr['repo_shortname']}:\n"
f"{jsonresp2['message']}"
)
@@ -363,9 +354,8 @@ class HacktoberStats(commands.Cog):
# loop through reviews and check for approval
for item in jsonresp2:
- if "status" in item.keys():
- if item['status'] == "APPROVED":
- return True
+ if item.get("status") == "APPROVED":
+ return True
return False
@staticmethod
@@ -381,8 +371,7 @@ class HacktoberStats(commands.Cog):
exp = r"https?:\/\/api.github.com\/repos\/([/\-\_\.\w]+)"
return re.findall(exp, in_url)[0]
- @staticmethod
- async def _categorize_prs(prs: List[dict]) -> tuple:
+ async def _categorize_prs(self, prs: List[dict]) -> tuple:
"""
Categorize PRs into 'in_review' and 'accepted' and returns as a tuple.
@@ -397,9 +386,9 @@ class HacktoberStats(commands.Cog):
in_review = []
accepted = []
for pr in prs:
- if (pr['created_at'] + timedelta(REVIEW_DAYS)) > now:
+ if (pr["created_at"] + timedelta(REVIEW_DAYS)) > now:
in_review.append(pr)
- elif (pr['created_at'] <= oct3) or await HacktoberStats._is_accepted(pr):
+ elif (pr["created_at"] <= oct3) or await self._is_accepted(pr):
accepted.append(pr)
return in_review, accepted
@@ -438,14 +427,14 @@ class HacktoberStats(commands.Cog):
return "contributions"
@staticmethod
- def _author_mention_from_context(ctx: commands.Context) -> Tuple:
+ def _author_mention_from_context(ctx: commands.Context) -> Tuple[str, str]:
"""Return stringified Message author ID and mentionable string from commands.Context."""
- author_id = str(ctx.message.author.id)
- author_mention = ctx.message.author.mention
+ author_id = str(ctx.author.id)
+ author_mention = ctx.author.mention
return author_id, author_mention
-def setup(bot: commands.Bot) -> None:
- """Hacktoberstats Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Hacktober Stats Cog."""
bot.add_cog(HacktoberStats(bot))
diff --git a/bot/exts/halloween/halloween_facts.py b/bot/exts/halloween/halloween_facts.py
index 7eb6d56f..5ad8cc57 100644
--- a/bot/exts/halloween/halloween_facts.py
+++ b/bot/exts/halloween/halloween_facts.py
@@ -8,6 +8,8 @@ from typing import Tuple
import discord
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
SPOOKY_EMOJIS = [
@@ -20,23 +22,19 @@ SPOOKY_EMOJIS = [
"\N{SKULL AND CROSSBONES}",
"\N{SPIDER WEB}",
]
-PUMPKIN_ORANGE = discord.Color(0xFF7518)
+PUMPKIN_ORANGE = 0xFF7518
INTERVAL = timedelta(hours=6).total_seconds()
+FACTS = json.loads(Path("bot/resources/halloween/halloween_facts.json").read_text("utf8"))
+FACTS = list(enumerate(FACTS))
+
class HalloweenFacts(commands.Cog):
"""A Cog for displaying interesting facts about Halloween."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- with open(Path("bot/resources/halloween/halloween_facts.json"), "r", encoding="utf8") as file:
- self.halloween_facts = json.load(file)
- self.facts = list(enumerate(self.halloween_facts))
- random.shuffle(self.facts)
-
def random_fact(self) -> Tuple[int, str]:
"""Return a random fact from the loaded facts."""
- return random.choice(self.facts)
+ return random.choice(FACTS)
@commands.command(name="spookyfact", aliases=("halloweenfact",), brief="Get the most recent Halloween fact")
async def get_random_fact(self, ctx: commands.Context) -> None:
@@ -53,6 +51,6 @@ class HalloweenFacts(commands.Cog):
return discord.Embed(title=title, description=fact, color=PUMPKIN_ORANGE)
-def setup(bot: commands.Bot) -> None:
- """Halloween facts Cog load."""
- bot.add_cog(HalloweenFacts(bot))
+def setup(bot: Bot) -> None:
+ """Load the Halloween Facts Cog."""
+ bot.add_cog(HalloweenFacts())
diff --git a/bot/exts/halloween/halloweenify.py b/bot/exts/halloween/halloweenify.py
index 596c6682..83cfbaa7 100644
--- a/bot/exts/halloween/halloweenify.py
+++ b/bot/exts/halloween/halloweenify.py
@@ -1,42 +1,40 @@
import logging
-from json import load
+from json import loads
from pathlib import Path
from random import choice
import discord
from discord.errors import Forbidden
from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from discord.ext.commands import BucketType
+
+from bot.bot import Bot
log = logging.getLogger(__name__)
+HALLOWEENIFY_DATA = loads(Path("bot/resources/halloween/halloweenify.json").read_text("utf8"))
+
class Halloweenify(commands.Cog):
"""A cog to change a invokers nickname to a spooky one!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.cooldown(1, 300, BucketType.user)
@commands.command()
async def halloweenify(self, ctx: commands.Context) -> None:
"""Change your nickname into a much spookier one!"""
async with ctx.typing():
- with open(Path("bot/resources/halloween/halloweenify.json"), "r", encoding="utf8") as f:
- data = load(f)
-
# Choose a random character from our list we loaded above and set apart the nickname and image url.
- character = choice(data["characters"])
- nickname = ''.join([nickname for nickname in character])
- image = ''.join([character[nickname] for nickname in character])
+ character = choice(HALLOWEENIFY_DATA["characters"])
+ nickname = "".join(nickname for nickname in character)
+ image = "".join(character[nickname] for nickname in character)
# Build up a Embed
embed = discord.Embed()
embed.colour = discord.Colour.dark_orange()
embed.title = "Not spooky enough?"
embed.description = (
- f"**{ctx.author.display_name}** wasn\'t spooky enough for you? That\'s understandable, "
- f"{ctx.author.display_name} isn\'t scary at all! "
+ f"**{ctx.author.display_name}** wasn't spooky enough for you? That's understandable, "
+ f"{ctx.author.display_name} isn't scary at all! "
"Let me think of something better. Hmm... I got it!\n\n "
)
embed.set_image(url=image)
@@ -61,6 +59,6 @@ class Halloweenify(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Halloweenify Cog load."""
- bot.add_cog(Halloweenify(bot))
+def setup(bot: Bot) -> None:
+ """Load the Halloweenify Cog."""
+ bot.add_cog(Halloweenify())
diff --git a/bot/exts/halloween/monsterbio.py b/bot/exts/halloween/monsterbio.py
index 016a66d1..69e898cb 100644
--- a/bot/exts/halloween/monsterbio.py
+++ b/bot/exts/halloween/monsterbio.py
@@ -6,20 +6,19 @@ from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/halloween/monster.json"), "r", encoding="utf8") as f:
- TEXT_OPTIONS = json.load(f) # Data for a mad-lib style generation of text
+TEXT_OPTIONS = json.loads(
+ Path("bot/resources/halloween/monster.json").read_text("utf8")
+) # Data for a mad-lib style generation of text
class MonsterBio(commands.Cog):
"""A cog that generates a spooky monster biography."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
def generate_name(self, seeded_random: random.Random) -> str:
"""Generates a name (for either monster species or monster name)."""
n_candidate_strings = seeded_random.randint(2, len(TEXT_OPTIONS["monster_type"]))
@@ -28,7 +27,7 @@ class MonsterBio(commands.Cog):
@commands.command(brief="Sends your monster bio!")
async def monsterbio(self, ctx: commands.Context) -> None:
"""Sends a description of a monster."""
- seeded_random = random.Random(ctx.message.author.id) # Seed a local Random instance rather than the system one
+ seeded_random = random.Random(ctx.author.id) # Seed a local Random instance rather than the system one
name = self.generate_name(seeded_random)
species = self.generate_name(seeded_random)
@@ -39,7 +38,7 @@ class MonsterBio(commands.Cog):
continue
options = seeded_random.sample(TEXT_OPTIONS[key], value)
- words[key] = ' '.join(options)
+ words[key] = " ".join(options)
embed = discord.Embed(
title=f"{name}'s Biography",
@@ -50,6 +49,6 @@ class MonsterBio(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Monster bio Cog load."""
- bot.add_cog(MonsterBio(bot))
+def setup(bot: Bot) -> None:
+ """Load the Monster Bio Cog."""
+ bot.add_cog(MonsterBio())
diff --git a/bot/exts/halloween/monstersurvey.py b/bot/exts/halloween/monstersurvey.py
index 80196825..96cda11e 100644
--- a/bot/exts/halloween/monstersurvey.py
+++ b/bot/exts/halloween/monstersurvey.py
@@ -1,6 +1,6 @@
import json
import logging
-import os
+import pathlib
from discord import Embed
from discord.ext import commands
@@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Cog, Context
log = logging.getLogger(__name__)
EMOJIS = {
- 'SUCCESS': u'\u2705',
- 'ERROR': u'\u274C'
+ "SUCCESS": u"\u2705",
+ "ERROR": u"\u274C"
}
@@ -23,18 +23,15 @@ class MonsterSurvey(Cog):
Users may change their vote, but only their current vote will be counted.
"""
- def __init__(self, bot: Bot):
+ def __init__(self):
"""Initializes values for the bot to use within the voting commands."""
- self.bot = bot
- self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json')
- with open(self.registry_location, 'r', encoding="utf8") as jason:
- self.voter_registry = json.load(jason)
+ self.registry_path = pathlib.Path("bot", "resources", "halloween", "monstersurvey.json")
+ self.voter_registry = json.loads(self.registry_path.read_text("utf8"))
def json_write(self) -> None:
"""Write voting results to a local JSON file."""
log.info("Saved Monster Survey Results")
- with open(self.registry_location, 'w', encoding="utf8") as jason:
- json.dump(self.voter_registry, jason, indent=2)
+ self.registry_path.write_text(json.dumps(self.voter_registry, indent=2))
def cast_vote(self, id: int, monster: str) -> None:
"""
@@ -43,54 +40,55 @@ class MonsterSurvey(Cog):
If the user has already voted, their existing vote is removed.
"""
vr = self.voter_registry
- for m in vr.keys():
- if id not in vr[m]['votes'] and m == monster:
- vr[m]['votes'].append(id)
+ for m in vr:
+ if id not in vr[m]["votes"] and m == monster:
+ vr[m]["votes"].append(id)
else:
- if id in vr[m]['votes'] and m != monster:
- vr[m]['votes'].remove(id)
+ if id in vr[m]["votes"] and m != monster:
+ vr[m]["votes"].remove(id)
def get_name_by_leaderboard_index(self, n: int) -> str:
"""Return the monster at the specified leaderboard index."""
n = n - 1
vr = self.voter_registry
- top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
+ top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True)
name = top[n] if n >= 0 else None
return name
@commands.group(
- name='monster',
- aliases=('mon',)
+ name="monster",
+ aliases=("mon",)
)
async def monster_group(self, ctx: Context) -> None:
"""The base voting command. If nothing is called, then it will return an embed."""
if ctx.invoked_subcommand is None:
async with ctx.typing():
default_embed = Embed(
- title='Monster Voting',
+ title="Monster Voting",
color=0xFF6800,
- description='Vote for your favorite monster!'
+ description="Vote for your favorite monster!"
)
default_embed.add_field(
- name='.monster show monster_name(optional)',
- value='Show a specific monster. If none is listed, it will give you an error with valid choices.',
- inline=False)
+ name=".monster show monster_name(optional)",
+ value="Show a specific monster. If none is listed, it will give you an error with valid choices.",
+ inline=False
+ )
default_embed.add_field(
- name='.monster vote monster_name',
- value='Vote for a specific monster. You get one vote, but can change it at any time.',
+ name=".monster vote monster_name",
+ value="Vote for a specific monster. You get one vote, but can change it at any time.",
inline=False
)
default_embed.add_field(
- name='.monster leaderboard',
- value='Which monster has the most votes? This command will tell you.',
+ name=".monster leaderboard",
+ value="Which monster has the most votes? This command will tell you.",
inline=False
)
- default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}")
+ default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry)}")
await ctx.send(embed=default_embed)
@monster_group.command(
- name='vote'
+ name="vote"
)
async def monster_vote(self, ctx: Context, name: str = None) -> None:
"""
@@ -111,37 +109,37 @@ class MonsterSurvey(Cog):
name = name.lower()
vote_embed = Embed(
- name='Monster Voting',
+ name="Monster Voting",
color=0xFF6800
)
m = self.voter_registry.get(name)
if m is None:
- vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.'
+ vote_embed.description = f"You cannot vote for {name} because it's not in the running."
vote_embed.add_field(
- name='Use `.monster show {monster_name}` for more information on a specific monster',
- value='or use `.monster vote {monster}` to cast your vote for said monster.',
+ name="Use `.monster show {monster_name}` for more information on a specific monster",
+ value="or use `.monster vote {monster}` to cast your vote for said monster.",
inline=False
)
vote_embed.add_field(
- name='You may vote for or show the following monsters:',
- value=f"{', '.join(self.voter_registry.keys())}"
+ name="You may vote for or show the following monsters:",
+ value=", ".join(self.voter_registry.keys())
)
else:
self.cast_vote(ctx.author.id, name)
vote_embed.add_field(
- name='Vote successful!',
- value=f'You have successfully voted for {m["full_name"]}!',
+ name="Vote successful!",
+ value=f"You have successfully voted for {m['full_name']}!",
inline=False
)
- vote_embed.set_thumbnail(url=m['image'])
+ vote_embed.set_thumbnail(url=m["image"])
vote_embed.set_footer(text="Please note that any previous votes have been removed.")
self.json_write()
await ctx.send(embed=vote_embed)
@monster_group.command(
- name='show'
+ name="show"
)
async def monster_show(self, ctx: Context, name: str = None) -> None:
"""Shows the named monster. If one is not named, it sends the default voting embed instead."""
@@ -159,41 +157,43 @@ class MonsterSurvey(Cog):
m = self.voter_registry.get(name)
if not m:
- await ctx.send('That monster does not exist.')
+ await ctx.send("That monster does not exist.")
await ctx.invoke(self.monster_vote)
return
- embed = Embed(title=m['full_name'], color=0xFF6800)
- embed.add_field(name='Summary', value=m['summary'])
- embed.set_image(url=m['image'])
- embed.set_footer(text=f'To vote for this monster, type .monster vote {name}')
+ embed = Embed(title=m["full_name"], color=0xFF6800)
+ embed.add_field(name="Summary", value=m["summary"])
+ embed.set_image(url=m["image"])
+ embed.set_footer(text=f"To vote for this monster, type .monster vote {name}")
await ctx.send(embed=embed)
@monster_group.command(
- name='leaderboard',
- aliases=('lb',)
+ name="leaderboard",
+ aliases=("lb",)
)
async def monster_leaderboard(self, ctx: Context) -> None:
"""Shows the current standings."""
async with ctx.typing():
vr = self.voter_registry
- top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
- total_votes = sum(len(m['votes']) for m in self.voter_registry.values())
+ top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True)
+ total_votes = sum(len(m["votes"]) for m in self.voter_registry.values())
embed = Embed(title="Monster Survey Leader Board", color=0xFF6800)
for rank, m in enumerate(top):
- votes = len(vr[m]['votes'])
+ votes = len(vr[m]["votes"])
percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0
- embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}",
- value=(
- f"{votes} votes. {percentage:.1f}% of total votes.\n"
- f"Vote for this monster by typing "
- f"'.monster vote {m}'\n"
- f"Get more information on this monster by typing "
- f"'.monster show {m}'"
- ),
- inline=False)
+ embed.add_field(
+ name=f"{rank+1}. {vr[m]['full_name']}",
+ value=(
+ f"{votes} votes. {percentage:.1f}% of total votes.\n"
+ f"Vote for this monster by typing "
+ f"'.monster vote {m}'\n"
+ f"Get more information on this monster by typing "
+ f"'.monster show {m}'"
+ ),
+ inline=False
+ )
embed.set_footer(text="You can also vote by their rank number. '.monster vote {number}' ")
@@ -201,4 +201,5 @@ class MonsterSurvey(Cog):
def setup(bot: Bot) -> None:
- """Monster survey Cog load."""
+ """Load the Monster Survey Cog."""
+ bot.add_cog(MonsterSurvey())
diff --git a/bot/exts/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py
index 0807eca6..f4cf41db 100644
--- a/bot/exts/halloween/scarymovie.py
+++ b/bot/exts/halloween/scarymovie.py
@@ -2,24 +2,25 @@ import logging
import random
from os import environ
-import aiohttp
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-TMDB_API_KEY = environ.get('TMDB_API_KEY')
-TMDB_TOKEN = environ.get('TMDB_TOKEN')
+TMDB_API_KEY = environ.get("TMDB_API_KEY")
+TMDB_TOKEN = environ.get("TMDB_TOKEN")
class ScaryMovie(commands.Cog):
"""Selects a random scary movie and embeds info into Discord chat."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- @commands.command(name='scarymovie', alias=['smovie'])
+ @commands.command(name="scarymovie", alias=["smovie"])
async def random_movie(self, ctx: commands.Context) -> None:
"""Randomly select a scary movie and display information about it."""
async with ctx.typing():
@@ -28,36 +29,34 @@ class ScaryMovie(commands.Cog):
await ctx.send(embed=movie_details)
- @staticmethod
- async def select_movie() -> dict:
+ async def select_movie(self) -> dict:
"""Selects a random movie and returns a JSON of movie details from TMDb."""
- url = 'https://api.themoviedb.org/4/discover/movie'
+ url = "https://api.themoviedb.org/4/discover/movie"
params = {
- 'with_genres': '27',
- 'vote_count.gte': '5'
+ "with_genres": "27",
+ "vote_count.gte": "5"
}
headers = {
- 'Authorization': 'Bearer ' + TMDB_TOKEN,
- 'Content-Type': 'application/json;charset=utf-8'
+ "Authorization": "Bearer " + TMDB_TOKEN,
+ "Content-Type": "application/json;charset=utf-8"
}
# Get total page count of horror movies
- async with aiohttp.ClientSession() as session:
- response = await session.get(url=url, params=params, headers=headers)
- total_pages = await response.json()
- total_pages = total_pages.get('total_pages')
-
- # Get movie details from one random result on a random page
- params['page'] = random.randint(1, total_pages)
- response = await session.get(url=url, params=params, headers=headers)
- response = await response.json()
- selection_id = random.choice(response.get('results')).get('id')
-
- # Get full details and credits
- selection = await session.get(
- url='https://api.themoviedb.org/3/movie/' + str(selection_id),
- params={'api_key': TMDB_API_KEY, 'append_to_response': 'credits'}
- )
+ async with self.bot.http_session.get(url=url, params=params, headers=headers) as response:
+ data = await response.json()
+ total_pages = data.get("total_pages")
+
+ # Get movie details from one random result on a random page
+ params["page"] = random.randint(1, total_pages)
+ async with self.bot.http_session.get(url=url, params=params, headers=headers) as response:
+ data = await response.json()
+ selection_id = random.choice(data.get("results")).get("id")
+
+ # Get full details and credits
+ async with self.bot.http_session.get(
+ url=f"https://api.themoviedb.org/3/movie/{selection_id}",
+ params={"api_key": TMDB_API_KEY, "append_to_response": "credits"}
+ ) as selection:
return await selection.json()
@@ -67,40 +66,37 @@ class ScaryMovie(commands.Cog):
# Build the relevant URLs.
movie_id = movie.get("id")
poster_path = movie.get("poster_path")
- tmdb_url = f'https://www.themoviedb.org/movie/{movie_id}' if movie_id else None
- poster = f'https://image.tmdb.org/t/p/original{poster_path}' if poster_path else None
+ tmdb_url = f"https://www.themoviedb.org/movie/{movie_id}" if movie_id else None
+ poster = f"https://image.tmdb.org/t/p/original{poster_path}" if poster_path else None
# Get cast names
cast = []
- for actor in movie.get('credits', {}).get('cast', [])[:3]:
- cast.append(actor.get('name'))
+ for actor in movie.get("credits", {}).get("cast", [])[:3]:
+ cast.append(actor.get("name"))
# Get director name
- director = movie.get('credits', {}).get('crew', [])
+ director = movie.get("credits", {}).get("crew", [])
if director:
- director = director[0].get('name')
+ director = director[0].get("name")
# Determine the spookiness rating
- rating = ''
- rating_count = movie.get('vote_average', 0)
-
- if rating_count:
- rating_count /= 2
+ rating = ""
+ rating_count = movie.get("vote_average", 0) / 2
for _ in range(int(rating_count)):
- rating += ':skull:'
+ rating += ":skull:"
if (rating_count % 1) >= .5:
- rating += ':bat:'
+ rating += ":bat:"
# Try to get year of release and runtime
- year = movie.get('release_date', [])[:4]
- runtime = movie.get('runtime')
+ year = movie.get("release_date", [])[:4]
+ runtime = movie.get("runtime")
runtime = f"{runtime} minutes" if runtime else None
# Not all these attributes will always be present
movie_attributes = {
"Directed by": director,
- "Starring": ', '.join(cast),
+ "Starring": ", ".join(cast),
"Running time": runtime,
"Release year": year,
"Spookiness rating": rating,
@@ -108,9 +104,9 @@ class ScaryMovie(commands.Cog):
embed = Embed(
colour=0x01d277,
- title='**' + movie.get('title') + '**',
+ title=f"**{movie.get('title')}**",
url=tmdb_url,
- description=movie.get('overview')
+ description=movie.get("overview")
)
if poster:
@@ -127,6 +123,6 @@ class ScaryMovie(commands.Cog):
return embed
-def setup(bot: commands.Bot) -> None:
- """Scary movie Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Scary Movie Cog."""
bot.add_cog(ScaryMovie(bot))
diff --git a/bot/exts/halloween/spookygif.py b/bot/exts/halloween/spookygif.py
index f402437f..9511d407 100644
--- a/bot/exts/halloween/spookygif.py
+++ b/bot/exts/halloween/spookygif.py
@@ -1,38 +1,38 @@
import logging
-import aiohttp
import discord
from discord.ext import commands
-from bot.constants import Tokens
+from bot.bot import Bot
+from bot.constants import Colours, Tokens
log = logging.getLogger(__name__)
+API_URL = "http://api.giphy.com/v1/gifs/random"
+
class SpookyGif(commands.Cog):
"""A cog to fetch a random spooky gif from the web!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.command(name="spookygif", aliases=("sgif", "scarygif"))
async def spookygif(self, ctx: commands.Context) -> None:
"""Fetches a random gif from the GIPHY API and responds with it."""
async with ctx.typing():
- async with aiohttp.ClientSession() as session:
- params = {'api_key': Tokens.giphy, 'tag': 'halloween', 'rating': 'g'}
- # Make a GET request to the Giphy API to get a random halloween gif.
- async with session.get('http://api.giphy.com/v1/gifs/random', params=params) as resp:
- data = await resp.json()
- url = data['data']['image_url']
+ params = {"api_key": Tokens.giphy, "tag": "halloween", "rating": "g"}
+ # Make a GET request to the Giphy API to get a random halloween gif.
+ async with self.bot.http_session.get(API_URL, params=params) as resp:
+ data = await resp.json()
+ url = data["data"]["image_url"]
- embed = discord.Embed(colour=0x9b59b6)
- embed.title = "A spooooky gif!"
- embed.set_image(url=url)
+ embed = discord.Embed(title="A spooooky gif!", colour=Colours.purple)
+ embed.set_image(url=url)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Spooky GIF Cog load."""
bot.add_cog(SpookyGif(bot))
diff --git a/bot/exts/halloween/spookynamerate.py b/bot/exts/halloween/spookynamerate.py
index e2950343..3d6d95fa 100644
--- a/bot/exts/halloween/spookynamerate.py
+++ b/bot/exts/halloween/spookynamerate.py
@@ -6,14 +6,15 @@ from datetime import datetime, timedelta
from logging import getLogger
from os import getenv
from pathlib import Path
-from typing import Dict, Union
+from typing import Union
from async_rediscache import RedisCache
from discord import Embed, Reaction, TextChannel, User
from discord.colour import Colour
from discord.ext import tasks
-from discord.ext.commands import Bot, Cog, Context, group
+from discord.ext.commands import Cog, Context, group
+from bot.bot import Bot
from bot.constants import Channels, Client, Colours, Month
from bot.utils.decorators import InMonthCheckFailure
@@ -34,7 +35,7 @@ ADDED_MESSAGES = [
]
PING = "<@{id}>"
-EMOJI_MESSAGE = "\n".join([f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items()])
+EMOJI_MESSAGE = "\n".join(f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items())
HELP_MESSAGE_DICT = {
"title": "Spooky Name Rate",
"description": f"Help for the `{Client.prefix}spookynamerate` command",
@@ -64,6 +65,11 @@ HELP_MESSAGE_DICT = {
],
}
+# The names are from https://www.mockaroo.com/
+NAMES = json.loads(Path("bot/resources/halloween/spookynamerate_names.json").read_text("utf8"))
+FIRST_NAMES = NAMES["first_names"]
+LAST_NAMES = NAMES["last_names"]
+
class SpookyNameRate(Cog):
"""
@@ -80,21 +86,13 @@ class SpookyNameRate(Cog):
# The data cache stores small information such as the current name that is going on and whether it is the first time
# the bot is running
data = RedisCache()
- debug = getenv('SPOOKYNAMERATE_DEBUG', False) # Enable if you do not want to limit the commands to October or if
+ debug = getenv("SPOOKYNAMERATE_DEBUG", False) # Enable if you do not want to limit the commands to October or if
# you do not want to wait till 12 UTC. Note: if debug is enabled and you run `.cogs reload spookynamerate`, it
# will automatically start the scoring and announcing the result (without waiting for 12, so do not expect it to.).
# Also, it won't wait for the two hours (when the poll closes).
def __init__(self, bot: Bot) -> None:
self.bot = bot
-
- names_data = self.load_json(
- Path("bot", "resources", "halloween", "spookynamerate_names.json")
- )
- self.first_names = names_data["first_names"]
- self.last_names = names_data["last_names"]
- # the names are from https://www.mockaroo.com/
-
self.name = None
self.bot.loop.create_task(self.load_vars())
@@ -116,7 +114,7 @@ class SpookyNameRate(Cog):
"""Get help on the Spooky Name Rate game."""
await ctx.send(embed=Embed.from_dict(HELP_MESSAGE_DICT))
- @spooky_name_rate.command(name="list", aliases=["all", "entries"])
+ @spooky_name_rate.command(name="list", aliases=("all", "entries"))
async def list_entries(self, ctx: Context) -> None:
"""Send all the entries up till now in a single embed."""
await ctx.send(embed=await self.get_responses_list(final=False))
@@ -133,18 +131,16 @@ class SpookyNameRate(Cog):
"add an entry."
)
- @spooky_name_rate.command(name="add", aliases=["register"])
+ @spooky_name_rate.command(name="add", aliases=("register",))
async def add_name(self, ctx: Context, *, name: str) -> None:
"""Use this command to add/register your spookified name."""
if self.poll:
- logger.info(f"{ctx.message.author} tried to add a name, but the poll had already started.")
+ logger.info(f"{ctx.author} tried to add a name, but the poll had already started.")
await ctx.send("Sorry, the poll has started! You can try and participate in the next round though!")
return
- message = ctx.message
-
for data in (json.loads(user_data) for _, user_data in await self.messages.items()):
- if data["author"] == message.author.id:
+ if data["author"] == ctx.author.id:
await ctx.send(
"But you have already added an entry! Type "
f"`{self.bot.command_prefix}spookynamerate "
@@ -156,14 +152,14 @@ class SpookyNameRate(Cog):
await ctx.send("TOO LATE. Someone has already added this name.")
return
- msg = await (await self.get_channel()).send(f"{message.author.mention} added the name {name!r}!")
+ msg = await (await self.get_channel()).send(f"{ctx.author.mention} added the name {name!r}!")
await self.messages.set(
msg.id,
json.dumps(
{
"name": name,
- "author": message.author.id,
+ "author": ctx.author.id,
"score": 0,
}
),
@@ -172,7 +168,7 @@ class SpookyNameRate(Cog):
for emoji in EMOJIS_VAL:
await msg.add_reaction(emoji)
- logger.info(f"{message.author} added the name {name!r}")
+ logger.info(f"{ctx.author} added the name {name!r}")
@spooky_name_rate.command(name="delete")
async def delete_name(self, ctx: Context) -> None:
@@ -185,7 +181,7 @@ class SpookyNameRate(Cog):
if ctx.author.id == data["author"]:
await self.messages.delete(message_id)
- await ctx.send(f'Name deleted successfully ({data["name"]!r})!')
+ await ctx.send(f"Name deleted successfully ({data['name']!r})!")
return
await ctx.send(
@@ -303,7 +299,7 @@ class SpookyNameRate(Cog):
await self.messages.clear() # reset the messages
# send the next name
- self.name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}"
+ self.name = f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"
await self.data.set("name", self.name)
await channel.send(
@@ -370,12 +366,6 @@ class SpookyNameRate(Cog):
return channel
@staticmethod
- def load_json(file: Path) -> Dict[str, str]:
- """Loads a JSON file and returns its contents."""
- with file.open("r", encoding="utf-8") as f:
- return json.load(f)
-
- @staticmethod
def in_allowed_month() -> bool:
"""Returns whether running in the limited month."""
if SpookyNameRate.debug:
@@ -397,5 +387,5 @@ class SpookyNameRate(Cog):
def setup(bot: Bot) -> None:
- """Loads the SpookyNameRate Cog."""
+ """Load the SpookyNameRate Cog."""
bot.add_cog(SpookyNameRate(bot))
diff --git a/bot/exts/halloween/spookyrating.py b/bot/exts/halloween/spookyrating.py
index 6f069f8c..105d2164 100644
--- a/bot/exts/halloween/spookyrating.py
+++ b/bot/exts/halloween/spookyrating.py
@@ -3,24 +3,24 @@ import json
import logging
import random
from pathlib import Path
+from typing import Dict
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with Path("bot/resources/halloween/spooky_rating.json").open(encoding="utf8") as file:
- SPOOKY_DATA = json.load(file)
- SPOOKY_DATA = sorted((int(key), value) for key, value in SPOOKY_DATA.items())
+data: Dict[str, Dict[str, str]] = json.loads(Path("bot/resources/halloween/spooky_rating.json").read_text("utf8"))
+SPOOKY_DATA = sorted((int(key), value) for key, value in data.items())
class SpookyRating(commands.Cog):
"""A cog for calculating one's spooky rating."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
+ def __init__(self):
self.local_random = random.Random()
@commands.command()
@@ -46,21 +46,21 @@ class SpookyRating(commands.Cog):
_, data = SPOOKY_DATA[index]
embed = discord.Embed(
- title=data['title'],
- description=f'{who} scored {spooky_percent}%!',
+ title=data["title"],
+ description=f"{who} scored {spooky_percent}%!",
color=Colours.orange
)
embed.add_field(
- name='A whisper from Satan',
- value=data['text']
+ name="A whisper from Satan",
+ value=data["text"]
)
embed.set_thumbnail(
- url=data['image']
+ url=data["image"]
)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Spooky Rating Cog load."""
- bot.add_cog(SpookyRating(bot))
+def setup(bot: Bot) -> None:
+ """Load the Spooky Rating Cog."""
+ bot.add_cog(SpookyRating())
diff --git a/bot/exts/halloween/spookyreact.py b/bot/exts/halloween/spookyreact.py
index b335df75..25e783f4 100644
--- a/bot/exts/halloween/spookyreact.py
+++ b/bot/exts/halloween/spookyreact.py
@@ -2,21 +2,22 @@ import logging
import re
import discord
-from discord.ext.commands import Bot, Cog
+from discord.ext.commands import Cog
+from bot.bot import Bot
from bot.constants import Month
from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
SPOOKY_TRIGGERS = {
- 'spooky': (r"\bspo{2,}ky\b", "\U0001F47B"),
- 'skeleton': (r"\bskeleton\b", "\U0001F480"),
- 'doot': (r"\bdo{2,}t\b", "\U0001F480"),
- 'pumpkin': (r"\bpumpkin\b", "\U0001F383"),
- 'halloween': (r"\bhalloween\b", "\U0001F383"),
- 'jack-o-lantern': (r"\bjack-o-lantern\b", "\U0001F383"),
- 'danger': (r"\bdanger\b", "\U00002620")
+ "spooky": (r"\bspo{2,}ky\b", "\U0001F47B"),
+ "skeleton": (r"\bskeleton\b", "\U0001F480"),
+ "doot": (r"\bdo{2,}t\b", "\U0001F480"),
+ "pumpkin": (r"\bpumpkin\b", "\U0001F383"),
+ "halloween": (r"\bhalloween\b", "\U0001F383"),
+ "jack-o-lantern": (r"\bjack-o-lantern\b", "\U0001F383"),
+ "danger": (r"\bdanger\b", "\U00002620")
}
@@ -28,20 +29,20 @@ class SpookyReact(Cog):
@in_month(Month.OCTOBER)
@Cog.listener()
- async def on_message(self, ctx: discord.Message) -> None:
+ async def on_message(self, message: discord.Message) -> None:
"""Triggered when the bot sees a message in October."""
- for trigger in SPOOKY_TRIGGERS.keys():
- trigger_test = re.search(SPOOKY_TRIGGERS[trigger][0], ctx.content.lower())
+ for name, trigger in SPOOKY_TRIGGERS.items():
+ trigger_test = re.search(trigger[0], message.content.lower())
if trigger_test:
# Check message for bot replies and/or command invocations
# Short circuit if they're found, logging is handled in _short_circuit_check
- if await self._short_circuit_check(ctx):
+ if await self._short_circuit_check(message):
return
else:
- await ctx.add_reaction(SPOOKY_TRIGGERS[trigger][1])
- logging.info(f"Added '{trigger}' reaction to message ID: {ctx.id}")
+ await message.add_reaction(trigger[1])
+ log.info(f"Added {name!r} reaction to message ID: {message.id}")
- async def _short_circuit_check(self, ctx: discord.Message) -> bool:
+ async def _short_circuit_check(self, message: discord.Message) -> bool:
"""
Short-circuit helper check.
@@ -50,20 +51,20 @@ class SpookyReact(Cog):
* prefix is not None
"""
# Check for self reaction
- if ctx.author == self.bot.user:
- logging.debug(f"Ignoring reactions on self message. Message ID: {ctx.id}")
+ if message.author == self.bot.user:
+ log.debug(f"Ignoring reactions on self message. Message ID: {message.id}")
return True
# Check for command invocation
# Because on_message doesn't give a full Context object, generate one first
- tmp_ctx = await self.bot.get_context(ctx)
- if tmp_ctx.prefix:
- logging.debug(f"Ignoring reactions on command invocation. Message ID: {ctx.id}")
+ ctx = await self.bot.get_context(message)
+ if ctx.prefix:
+ log.debug(f"Ignoring reactions on command invocation. Message ID: {message.id}")
return True
return False
def setup(bot: Bot) -> None:
- """Spooky reaction Cog load."""
+ """Load the Spooky Reaction Cog."""
bot.add_cog(SpookyReact(bot))
diff --git a/bot/exts/halloween/timeleft.py b/bot/exts/halloween/timeleft.py
index 47adb09b..e80025dc 100644
--- a/bot/exts/halloween/timeleft.py
+++ b/bot/exts/halloween/timeleft.py
@@ -4,15 +4,14 @@ from typing import Tuple
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
class TimeLeft(commands.Cog):
"""A Cog that tells you how long left until Hacktober is over!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
def in_hacktober(self) -> bool:
"""Return True if the current time is within Hacktoberfest."""
_, end, start = self.load_date()
@@ -64,6 +63,6 @@ class TimeLeft(commands.Cog):
)
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(TimeLeft(bot))
+def setup(bot: Bot) -> None:
+ """Load the Time Left Cog."""
+ bot.add_cog(TimeLeft())
diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py
index 757a2a1e..56bf5add 100644
--- a/bot/exts/internal_eval/_internal_eval.py
+++ b/bot/exts/internal_eval/_internal_eval.py
@@ -114,7 +114,7 @@ class InternalEval(commands.Cog):
"""Evaluate the `code` in the current evaluation context."""
context_vars = {
"message": ctx.message,
- "author": ctx.message.author,
+ "author": ctx.author,
"channel": ctx.channel,
"guild": ctx.guild,
"ctx": ctx,
@@ -142,14 +142,14 @@ class InternalEval(commands.Cog):
log.trace("Sending the formatted output back to the context")
await self._send_output(ctx, eval_context.format_output())
- @commands.group(name='internal', aliases=('int',))
+ @commands.group(name="internal", aliases=("int",))
@with_role(Roles.admin)
async def internal_group(self, ctx: commands.Context) -> None:
"""Internal commands. Top secret!"""
if not ctx.invoked_subcommand:
await invoke_help_command(ctx)
- @internal_group.command(name='eval', aliases=('e',))
+ @internal_group.command(name="eval", aliases=("e",))
@with_role(Roles.admin)
async def eval(self, ctx: commands.Context, *, code: str) -> None:
"""Run eval in a REPL-like format."""
@@ -157,7 +157,7 @@ class InternalEval(commands.Cog):
blocks = [block for block in match if block.group("block")]
if len(blocks) > 1:
- code = '\n'.join(block.group("code") for block in blocks)
+ code = "\n".join(block.group("code") for block in blocks)
else:
match = match[0] if len(blocks) == 0 else blocks[0]
code, block, lang, delim = match.group("code", "block", "lang", "delim")
@@ -168,7 +168,7 @@ class InternalEval(commands.Cog):
code = textwrap.dedent(code)
await self._eval(ctx, code)
- @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c"))
+ @internal_group.command(name="reset", aliases=("clear", "exit", "r", "c"))
@with_role(Roles.admin)
async def reset(self, ctx: commands.Context) -> None:
"""Reset the context and locals of the eval session."""
diff --git a/bot/exts/pride/drag_queen_name.py b/bot/exts/pride/drag_queen_name.py
index fca9750f..15ca6576 100644
--- a/bot/exts/pride/drag_queen_name.py
+++ b/bot/exts/pride/drag_queen_name.py
@@ -5,28 +5,22 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
+NAMES = json.loads(Path("bot/resources/pride/drag_queen_names.json").read_text("utf8"))
+
class DragNames(commands.Cog):
"""Gives a random drag queen name!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.names = self.load_names()
-
- @staticmethod
- def load_names() -> list:
- """Loads a list of drag queen names."""
- with open(Path("bot/resources/pride/drag_queen_names.json"), "r", encoding="utf8") as f:
- return json.load(f)
-
- @commands.command(name="dragname", aliases=["dragqueenname", "queenme"])
+ @commands.command(name="dragname", aliases=("dragqueenname", "queenme"))
async def dragname(self, ctx: commands.Context) -> None:
"""Sends a message with a drag queen name."""
- await ctx.send(random.choice(self.names))
+ await ctx.send(random.choice(NAMES))
-def setup(bot: commands.Bot) -> None:
- """Cog loader for drag queen name generator."""
- bot.add_cog(DragNames(bot))
+def setup(bot: Bot) -> None:
+ """Load the Drag Names Cog."""
+ bot.add_cog(DragNames())
diff --git a/bot/exts/pride/pride_anthem.py b/bot/exts/pride/pride_anthem.py
index 33cb2a9d..4650595a 100644
--- a/bot/exts/pride/pride_anthem.py
+++ b/bot/exts/pride/pride_anthem.py
@@ -2,20 +2,21 @@ import json
import logging
import random
from pathlib import Path
+from typing import Optional
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
+VIDEOS = json.loads(Path("bot/resources/pride/anthems.json").read_text("utf8"))
+
class PrideAnthem(commands.Cog):
"""Embed a random youtube video for a gay anthem!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.anthems = self.load_vids()
-
- def get_video(self, genre: str = None) -> dict:
+ def get_video(self, genre: Optional[str] = None) -> dict:
"""
Picks a random anthem from the list.
@@ -25,20 +26,13 @@ class PrideAnthem(commands.Cog):
if not genre:
return random.choice(self.anthems)
else:
- songs = [song for song in self.anthems if genre.casefold() in song["genre"]]
+ songs = [song for song in VIDEOS if genre.casefold() in song["genre"]]
try:
return random.choice(songs)
except IndexError:
log.info("No videos for that genre.")
- @staticmethod
- def load_vids() -> list:
- """Loads a list of videos from the resources folder as dictionaries."""
- with open(Path("bot/resources/pride/anthems.json"), "r", encoding="utf8") as f:
- anthems = json.load(f)
- return anthems
-
- @commands.command(name="prideanthem", aliases=["anthem", "pridesong"])
+ @commands.command(name="prideanthem", aliases=("anthem", "pridesong"))
async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None:
"""
Sends a message with a video of a random pride anthem.
@@ -52,6 +46,6 @@ class PrideAnthem(commands.Cog):
await ctx.send("I couldn't find a video, sorry!")
-def setup(bot: commands.Bot) -> None:
- """Cog loader for pride anthem."""
- bot.add_cog(PrideAnthem(bot))
+def setup(bot: Bot) -> None:
+ """Load the Pride Anthem Cog."""
+ bot.add_cog(PrideAnthem())
diff --git a/bot/exts/pride/pride_facts.py b/bot/exts/pride/pride_facts.py
index 5bd5d0ce..631e2e8b 100644
--- a/bot/exts/pride/pride_facts.py
+++ b/bot/exts/pride/pride_facts.py
@@ -15,7 +15,7 @@ from bot.utils.decorators import seasonal_task
log = logging.getLogger(__name__)
-Sendable = Union[commands.Context, discord.TextChannel]
+FACTS = json.loads(Path("bot/resources/pride/facts.json").read_text("utf8"))
class PrideFacts(commands.Cog):
@@ -23,16 +23,8 @@ class PrideFacts(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.facts = self.load_facts()
-
self.daily_fact_task = self.bot.loop.create_task(self.send_pride_fact_daily())
- @staticmethod
- def load_facts() -> dict:
- """Loads a dictionary of years mapping to lists of facts."""
- with open(Path("bot/resources/pride/facts.json"), "r", encoding="utf8") as f:
- return json.load(f)
-
@seasonal_task(Month.JUNE)
async def send_pride_fact_daily(self) -> None:
"""Background task to post the daily pride fact every day."""
@@ -44,15 +36,15 @@ class PrideFacts(commands.Cog):
async def send_random_fact(self, ctx: commands.Context) -> None:
"""Provides a fact from any previous day, or today."""
now = datetime.utcnow()
- previous_years_facts = (self.facts[x] for x in self.facts.keys() if int(x) < now.year)
- current_year_facts = self.facts.get(str(now.year), [])[:now.day]
+ previous_years_facts = (y for x, y in FACTS.items() if int(x) < now.year)
+ current_year_facts = FACTS.get(str(now.year), [])[:now.day]
previous_facts = current_year_facts + [x for y in previous_years_facts for x in y]
try:
await ctx.send(embed=self.make_embed(random.choice(previous_facts)))
except IndexError:
await ctx.send("No facts available")
- async def send_select_fact(self, target: Sendable, _date: Union[str, datetime]) -> None:
+ async def send_select_fact(self, target: discord.abc.Messageable, _date: Union[str, datetime]) -> None:
"""Provides the fact for the specified day, if the day is today, or is in the past."""
now = datetime.utcnow()
if isinstance(_date, str):
@@ -75,8 +67,8 @@ class PrideFacts(commands.Cog):
else:
await target.send("The fact for the selected day is not yet available.")
- @commands.command(name="pridefact", aliases=["pridefacts"])
- async def pridefact(self, ctx: commands.Context) -> None:
+ @commands.command(name="pridefact", aliases=("pridefacts",))
+ async def pridefact(self, ctx: commands.Context, option: str = None) -> None:
"""
Sends a message with a pride fact of the day.
@@ -85,15 +77,15 @@ class PrideFacts(commands.Cog):
If a date is given as an argument, and the date is in the past, the fact from that day
will be provided.
"""
- message_body = ctx.message.content[len(ctx.invoked_with) + 2:]
- if message_body == "":
+ if not option:
await self.send_select_fact(ctx, datetime.utcnow())
- elif message_body.lower().startswith("rand"):
+ elif option.lower().startswith("rand"):
await self.send_random_fact(ctx)
else:
- await self.send_select_fact(ctx, message_body)
+ await self.send_select_fact(ctx, option)
- def make_embed(self, fact: str) -> discord.Embed:
+ @staticmethod
+ def make_embed(fact: str) -> discord.Embed:
"""Makes a nice embed for the fact to be sent."""
return discord.Embed(
colour=Colours.pink,
@@ -103,5 +95,5 @@ class PrideFacts(commands.Cog):
def setup(bot: Bot) -> None:
- """Cog loader for pride facts."""
+ """Load the Pride Facts Cog."""
bot.add_cog(PrideFacts(bot))
diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py
index 09591cf8..8b522a72 100644
--- a/bot/exts/valentines/be_my_valentine.py
+++ b/bot/exts/valentines/be_my_valentine.py
@@ -1,13 +1,13 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
from typing import Tuple
import discord
from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from bot.bot import Bot
from bot.constants import Channels, Colours, Lovefest, Month
from bot.utils.decorators import in_month
from bot.utils.extensions import invoke_help_command
@@ -20,7 +20,7 @@ HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_hea
class BeMyValentine(commands.Cog):
"""A cog that sends Valentines to other users!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.valentines = self.load_json()
@@ -28,9 +28,7 @@ class BeMyValentine(commands.Cog):
def load_json() -> dict:
"""Load Valentines messages from the static resources."""
p = Path("bot/resources/valentines/bemyvalentine_valentines.json")
- with p.open(encoding="utf8") as json_data:
- valentines = load(json_data)
- return valentines
+ return loads(p.read_text("utf8"))
@in_month(Month.FEBRUARY)
@commands.group(name="lovefest")
@@ -50,8 +48,8 @@ class BeMyValentine(commands.Cog):
async def add_role(self, ctx: commands.Context) -> None:
"""Adds the lovefest role."""
user = ctx.author
- role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
- if Lovefest.role_id not in [role.id for role in ctx.message.author.roles]:
+ role = ctx.guild.get_role(Lovefest.role_id)
+ if role not in ctx.author.roles:
await user.add_roles(role)
await ctx.send("The Lovefest role has been added !")
else:
@@ -61,15 +59,15 @@ class BeMyValentine(commands.Cog):
async def remove_role(self, ctx: commands.Context) -> None:
"""Removes the lovefest role."""
user = ctx.author
- role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
- if Lovefest.role_id not in [role.id for role in ctx.message.author.roles]:
+ role = ctx.guild.get_role(Lovefest.role_id)
+ if role not in ctx.author.roles:
await ctx.send("You dont have the lovefest role.")
else:
await user.remove_roles(role)
- await ctx.send("The lovefest role has been successfully removed !")
+ await ctx.send("The lovefest role has been successfully removed!")
- @commands.cooldown(1, 1800, BucketType.user)
- @commands.group(name='bemyvalentine', invoke_without_command=True)
+ @commands.cooldown(1, 1800, commands.BucketType.user)
+ @commands.group(name="bemyvalentine", invoke_without_command=True)
async def send_valentine(
self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None
) -> None:
@@ -101,14 +99,14 @@ class BeMyValentine(commands.Cog):
valentine, title = self.valentine_check(valentine_type)
embed = discord.Embed(
- title=f'{emoji_1} {title} {user.display_name} {emoji_2}',
- description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**',
+ title=f"{emoji_1} {title} {user.display_name} {emoji_2}",
+ description=f"{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**",
color=Colours.pink
)
await channel.send(user.mention, embed=embed)
- @commands.cooldown(1, 1800, BucketType.user)
- @send_valentine.command(name='secret')
+ @commands.cooldown(1, 1800, commands.BucketType.user)
+ @send_valentine.command(name="secret")
async def anonymous(
self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None
) -> None:
@@ -136,8 +134,8 @@ class BeMyValentine(commands.Cog):
valentine, title = self.valentine_check(valentine_type)
embed = discord.Embed(
- title=f'{emoji_1}{title} {user.display_name}{emoji_2}',
- description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**',
+ title=f"{emoji_1}{title} {user.display_name}{emoji_2}",
+ description=f"{valentine} \n **{emoji_2}From anonymous{emoji_1}**",
color=Colours.pink
)
await ctx.message.delete()
@@ -151,21 +149,17 @@ class BeMyValentine(commands.Cog):
def valentine_check(self, valentine_type: str) -> Tuple[str, str]:
"""Return the appropriate Valentine type & title based on the invoking user's input."""
if valentine_type is None:
- valentine, title = self.random_valentine()
+ return self.random_valentine()
- elif valentine_type.lower() in ['p', 'poem']:
- valentine = self.valentine_poem()
- title = 'A poem dedicated to'
+ elif valentine_type.lower() in ["p", "poem"]:
+ return self.valentine_poem(), "A poem dedicated to"
- elif valentine_type.lower() in ['c', 'compliment']:
- valentine = self.valentine_compliment()
- title = 'A compliment for'
+ elif valentine_type.lower() in ["c", "compliment"]:
+ return self.valentine_compliment(), "A compliment for"
else:
# in this case, the user decides to type his own valentine.
- valentine = valentine_type
- title = 'A message for'
- return valentine, title
+ return valentine_type, "A message for"
@staticmethod
def random_emoji() -> Tuple[str, str]:
@@ -176,26 +170,24 @@ class BeMyValentine(commands.Cog):
def random_valentine(self) -> Tuple[str, str]:
"""Grabs a random poem or a compliment (any message)."""
- valentine_poem = random.choice(self.valentines['valentine_poems'])
- valentine_compliment = random.choice(self.valentines['valentine_compliments'])
+ valentine_poem = random.choice(self.valentines["valentine_poems"])
+ valentine_compliment = random.choice(self.valentines["valentine_compliments"])
random_valentine = random.choice([valentine_compliment, valentine_poem])
if random_valentine == valentine_poem:
- title = 'A poem dedicated to'
+ title = "A poem dedicated to"
else:
- title = 'A compliment for '
+ title = "A compliment for "
return random_valentine, title
def valentine_poem(self) -> str:
"""Grabs a random poem."""
- valentine_poem = random.choice(self.valentines['valentine_poems'])
- return valentine_poem
+ return random.choice(self.valentines["valentine_poems"])
def valentine_compliment(self) -> str:
"""Grabs a random compliment."""
- valentine_compliment = random.choice(self.valentines['valentine_compliments'])
- return valentine_compliment
+ return random.choice(self.valentines["valentine_compliments"])
-def setup(bot: commands.Bot) -> None:
- """Be my Valentine Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Be my Valentine Cog."""
bot.add_cog(BeMyValentine(bot))
diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py
index 966acc82..b10b7bca 100644
--- a/bot/exts/valentines/lovecalculator.py
+++ b/bot/exts/valentines/lovecalculator.py
@@ -11,20 +11,18 @@ from discord import Member
from discord.ext import commands
from discord.ext.commands import BadArgument, Cog, clean_content
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with Path("bot/resources/valentines/love_matches.json").open(encoding="utf8") as file:
- LOVE_DATA = json.load(file)
- LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())
+LOVE_DATA = json.loads(Path("bot/resources/valentines/love_matches.json").read_text("utf8"))
+LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())
class LoveCalculator(Cog):
"""A cog for calculating the love between two people."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('love_calculator', 'love_calc'))
+ @commands.command(aliases=("love_calculator", "love_calc"))
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def love(self, ctx: commands.Context, who: Union[Member, str], whom: Union[Member, str] = None) -> None:
"""
@@ -62,7 +60,7 @@ class LoveCalculator(Cog):
# Make sure user didn't provide something silly such as 10 spaces
if not (who and whom):
- raise BadArgument('Arguments be non-empty strings.')
+ raise BadArgument("Arguments must be non-empty strings.")
# Hash inputs to guarantee consistent results (hashing algorithm choice arbitrary)
#
@@ -79,20 +77,20 @@ class LoveCalculator(Cog):
# We only need the dict, so we can ditch the first element
_, data = LOVE_DATA[index]
- status = random.choice(data['titles'])
+ status = random.choice(data["titles"])
embed = discord.Embed(
title=status,
- description=f'{who} \N{HEAVY BLACK HEART} {whom} scored {love_percent}%!\n\u200b',
+ description=f"{who} \N{HEAVY BLACK HEART} {whom} scored {love_percent}%!\n\u200b",
color=discord.Color.dark_magenta()
)
embed.add_field(
- name='A letter from Dr. Love:',
- value=data['text']
+ name="A letter from Dr. Love:",
+ value=data["text"]
)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Love calculator Cog load."""
- bot.add_cog(LoveCalculator(bot))
+def setup(bot: Bot) -> None:
+ """Load the Love calculator Cog."""
+ bot.add_cog(LoveCalculator())
diff --git a/bot/exts/valentines/movie_generator.py b/bot/exts/valentines/movie_generator.py
index 4df9e0d5..0fc5edb4 100644
--- a/bot/exts/valentines/movie_generator.py
+++ b/bot/exts/valentines/movie_generator.py
@@ -6,6 +6,8 @@ from urllib import parse
import discord
from discord.ext import commands
+from bot.bot import Bot
+
TMDB_API_KEY = environ.get("TMDB_API_KEY")
log = logging.getLogger(__name__)
@@ -14,7 +16,7 @@ log = logging.getLogger(__name__)
class RomanceMovieFinder(commands.Cog):
"""A Cog that returns a random romance movie suggestion to a user."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.command(name="romancemovie")
@@ -52,13 +54,15 @@ class RomanceMovieFinder(commands.Cog):
embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png")
await ctx.send(embed=embed)
except KeyError:
- warning_message = "A KeyError was raised while fetching information on the movie. The API service" \
- " could be unavailable or the API key could be set incorrectly."
+ warning_message = (
+ "A KeyError was raised while fetching information on the movie. The API service"
+ " could be unavailable or the API key could be set incorrectly."
+ )
embed = discord.Embed(title=warning_message)
log.warning(warning_message)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Romance movie Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Romance movie Cog."""
bot.add_cog(RomanceMovieFinder(bot))
diff --git a/bot/exts/valentines/myvalenstate.py b/bot/exts/valentines/myvalenstate.py
index 01801847..52a61011 100644
--- a/bot/exts/valentines/myvalenstate.py
+++ b/bot/exts/valentines/myvalenstate.py
@@ -7,20 +7,17 @@ from random import choice
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/valenstates.json"), "r", encoding="utf8") as file:
- STATES = json.load(file)
+STATES = json.loads(Path("bot/resources/valentines/valenstates.json").read_text("utf8"))
class MyValenstate(commands.Cog):
"""A Cog to find your most likely Valentine's vacation destination."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
def levenshtein(self, source: str, goal: str) -> int:
"""Calculates the Levenshtein Distance between source and goal."""
if len(source) < len(goal):
@@ -46,12 +43,12 @@ class MyValenstate(commands.Cog):
"""Find the vacation spot(s) with the most matching characters to the invoking user."""
eq_chars = collections.defaultdict(int)
if name is None:
- author = ctx.message.author.name.lower().replace(' ', '')
+ author = ctx.author.name.lower().replace(" ", "")
else:
- author = name.lower().replace(' ', '')
+ author = name.lower().replace(" ", "")
for state in STATES.keys():
- lower_state = state.lower().replace(' ', '')
+ lower_state = state.lower().replace(" ", "")
eq_chars[state] = self.levenshtein(author, lower_state)
matches = [x for x, y in eq_chars.items() if y == min(eq_chars.values())]
@@ -60,27 +57,26 @@ class MyValenstate(commands.Cog):
embed_title = "But there are more!"
if len(matches) > 1:
- leftovers = f"{', '.join(matches[:len(matches)-2])}, and {matches[len(matches)-1]}"
+ leftovers = f"{', '.join(matches[:-2])}, and {matches[-1]}"
embed_text = f"You have {len(matches)} more matches, these being {leftovers}."
elif len(matches) == 1:
embed_title = "But there's another one!"
- leftovers = str(matches)
- embed_text = f"You have another match, this being {leftovers}."
+ embed_text = f"You have another match, this being {matches[0]}."
else:
embed_title = "You have a true match!"
embed_text = "This state is your true Valenstate! There are no states that would suit" \
" you better"
embed = discord.Embed(
- title=f'Your Valenstate is {valenstate} \u2764',
- description=f'{STATES[valenstate]["text"]}',
+ title=f"Your Valenstate is {valenstate} \u2764",
+ description=STATES[valenstate]["text"],
colour=Colours.pink
)
embed.add_field(name=embed_title, value=embed_text)
embed.set_image(url=STATES[valenstate]["flag"])
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Valenstate Cog load."""
- bot.add_cog(MyValenstate(bot))
+def setup(bot: Bot) -> None:
+ """Load the Valenstate Cog."""
+ bot.add_cog(MyValenstate())
diff --git a/bot/exts/valentines/pickuplines.py b/bot/exts/valentines/pickuplines.py
index 74c7e68b..00741a72 100644
--- a/bot/exts/valentines/pickuplines.py
+++ b/bot/exts/valentines/pickuplines.py
@@ -1,25 +1,22 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/pickup_lines.json"), "r", encoding="utf8") as f:
- pickup_lines = load(f)
+PICKUP_LINES = loads(Path("bot/resources/valentines/pickup_lines.json").read_text("utf8"))
class PickupLine(commands.Cog):
"""A cog that gives random cheesy pickup lines."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command()
async def pickupline(self, ctx: commands.Context) -> None:
"""
@@ -27,18 +24,18 @@ class PickupLine(commands.Cog):
Note that most of them are very cheesy.
"""
- random_line = random.choice(pickup_lines['lines'])
+ random_line = random.choice(PICKUP_LINES["lines"])
embed = discord.Embed(
- title=':cheese: Your pickup line :cheese:',
- description=random_line['line'],
+ title=":cheese: Your pickup line :cheese:",
+ description=random_line["line"],
color=Colours.pink
)
embed.set_thumbnail(
- url=random_line.get('image', pickup_lines['placeholder'])
+ url=random_line.get("image", PICKUP_LINES["placeholder"])
)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Pickup lines Cog load."""
- bot.add_cog(PickupLine(bot))
+def setup(bot: Bot) -> None:
+ """Load the Pickup lines Cog."""
+ bot.add_cog(PickupLine())
diff --git a/bot/exts/valentines/savethedate.py b/bot/exts/valentines/savethedate.py
index ac38d279..ffe559d6 100644
--- a/bot/exts/valentines/savethedate.py
+++ b/bot/exts/valentines/savethedate.py
@@ -1,31 +1,28 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
-with open(Path("bot/resources/valentines/date_ideas.json"), "r", encoding="utf8") as f:
- VALENTINES_DATES = load(f)
+VALENTINES_DATES = loads(Path("bot/resources/valentines/date_ideas.json").read_text("utf8"))
class SaveTheDate(commands.Cog):
"""A cog that gives random suggestion for a Valentine's date."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command()
async def savethedate(self, ctx: commands.Context) -> None:
"""Gives you ideas for what to do on a date with your valentine."""
- random_date = random.choice(VALENTINES_DATES['ideas'])
+ random_date = random.choice(VALENTINES_DATES["ideas"])
emoji_1 = random.choice(HEART_EMOJIS)
emoji_2 = random.choice(HEART_EMOJIS)
embed = discord.Embed(
@@ -36,6 +33,6 @@ class SaveTheDate(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Save the date Cog Load."""
- bot.add_cog(SaveTheDate(bot))
+def setup(bot: Bot) -> None:
+ """Load the Save the date Cog."""
+ bot.add_cog(SaveTheDate())
diff --git a/bot/exts/valentines/valentine_zodiac.py b/bot/exts/valentines/valentine_zodiac.py
index 2696999f..d862ee63 100644
--- a/bot/exts/valentines/valentine_zodiac.py
+++ b/bot/exts/valentines/valentine_zodiac.py
@@ -9,19 +9,19 @@ from typing import Tuple, Union
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-LETTER_EMOJI = ':love_letter:'
+LETTER_EMOJI = ":love_letter:"
HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
class ValentineZodiac(commands.Cog):
"""A Cog that returns a counter compatible zodiac sign to the given user's zodiac sign."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
+ def __init__(self):
self.zodiacs, self.zodiac_fact = self.load_comp_json()
@staticmethod
@@ -29,14 +29,14 @@ class ValentineZodiac(commands.Cog):
"""Load zodiac compatibility from static JSON resource."""
explanation_file = Path("bot/resources/valentines/zodiac_explanation.json")
compatibility_file = Path("bot/resources/valentines/zodiac_compatibility.json")
- with explanation_file.open(encoding="utf8") as json_data:
- zodiac_fact = json.load(json_data)
- for zodiac_data in zodiac_fact.values():
- zodiac_data['start_at'] = datetime.fromisoformat(zodiac_data['start_at'])
- zodiac_data['end_at'] = datetime.fromisoformat(zodiac_data['end_at'])
- with compatibility_file.open(encoding="utf8") as json_data:
- zodiacs = json.load(json_data)
+ zodiac_fact = json.loads(explanation_file.read_text("utf8"))
+
+ for zodiac_data in zodiac_fact.values():
+ zodiac_data["start_at"] = datetime.fromisoformat(zodiac_data["start_at"])
+ zodiac_data["end_at"] = datetime.fromisoformat(zodiac_data["end_at"])
+
+ zodiacs = json.loads(compatibility_file.read_text("utf8"))
return zodiacs, zodiac_fact
@@ -62,10 +62,10 @@ class ValentineZodiac(commands.Cog):
log.trace("Making zodiac embed.")
embed.title = f"__{zodiac}__"
embed.description = self.zodiac_fact[zodiac]["About"]
- embed.add_field(name='__Motto__', value=self.zodiac_fact[zodiac]["Motto"], inline=False)
- embed.add_field(name='__Strengths__', value=self.zodiac_fact[zodiac]["Strengths"], inline=False)
- embed.add_field(name='__Weaknesses__', value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False)
- embed.add_field(name='__Full form__', value=self.zodiac_fact[zodiac]["full_form"], inline=False)
+ embed.add_field(name="__Motto__", value=self.zodiac_fact[zodiac]["Motto"], inline=False)
+ embed.add_field(name="__Strengths__", value=self.zodiac_fact[zodiac]["Strengths"], inline=False)
+ embed.add_field(name="__Weaknesses__", value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False)
+ embed.add_field(name="__Full form__", value=self.zodiac_fact[zodiac]["full_form"], inline=False)
embed.set_thumbnail(url=self.zodiac_fact[zodiac]["url"])
else:
embed = self.generate_invalidname_embed(zodiac)
@@ -79,7 +79,7 @@ class ValentineZodiac(commands.Cog):
log.trace("Zodiac name sent.")
return zodiac_name
- @commands.group(name='zodiac', invoke_without_command=True)
+ @commands.group(name="zodiac", invoke_without_command=True)
async def zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None:
"""Provides information about zodiac sign by taking zodiac sign name as input."""
final_embed = self.zodiac_build_embed(zodiac_sign)
@@ -93,9 +93,9 @@ class ValentineZodiac(commands.Cog):
month = month.capitalize()
try:
month = list(calendar.month_abbr).index(month[:3])
- log.trace('Valid month name entered by user')
+ log.trace("Valid month name entered by user")
except ValueError:
- log.info('Invalid month name entered by user')
+ log.info("Invalid month name entered by user")
await ctx.send(f"Sorry, but `{month}` is not a valid month name.")
return
if (month == 1 and 1 <= date <= 19) or (month == 12 and 22 <= date <= 31):
@@ -109,14 +109,14 @@ class ValentineZodiac(commands.Cog):
final_embed = discord.Embed()
final_embed.color = Colours.soft_red
final_embed.description = f"Zodiac sign could not be found because.\n```{e}```"
- log.info(f'Error in "zodiac date" command:\n{e}.')
+ log.info(f"Error in 'zodiac date' command:\n{e}.")
else:
final_embed = self.zodiac_build_embed(zodiac_sign_based_on_date)
await ctx.send(embed=final_embed)
log.trace("Embed from date successfully sent.")
- @zodiac.command(name="partnerzodiac", aliases=['partner'])
+ @zodiac.command(name="partnerzodiac", aliases=("partner",))
async def partner_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None:
"""Provides a random counter compatible zodiac sign to the given user's zodiac sign."""
embed = discord.Embed()
@@ -128,12 +128,12 @@ class ValentineZodiac(commands.Cog):
emoji2 = random.choice(HEART_EMOJIS)
embed.title = "Zodiac Compatibility"
embed.description = (
- f'{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac["Zodiac"]}\n'
- f'{emoji2}Compatibility meter : {compatible_zodiac["compatibility_score"]}{emoji2}'
+ f"{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac['Zodiac']}\n"
+ f"{emoji2}Compatibility meter : {compatible_zodiac['compatibility_score']}{emoji2}"
)
embed.add_field(
- name=f'A letter from Dr.Zodiac {LETTER_EMOJI}',
- value=compatible_zodiac['description']
+ name=f"A letter from Dr.Zodiac {LETTER_EMOJI}",
+ value=compatible_zodiac["description"]
)
else:
embed = self.generate_invalidname_embed(zodiac_sign)
@@ -141,6 +141,6 @@ class ValentineZodiac(commands.Cog):
log.trace("Embed from date successfully sent.")
-def setup(bot: commands.Bot) -> None:
- """Valentine zodiac Cog load."""
- bot.add_cog(ValentineZodiac(bot))
+def setup(bot: Bot) -> None:
+ """Load the Valentine zodiac Cog."""
+ bot.add_cog(ValentineZodiac())
diff --git a/bot/exts/valentines/whoisvalentine.py b/bot/exts/valentines/whoisvalentine.py
index 0ff9186c..211b1f27 100644
--- a/bot/exts/valentines/whoisvalentine.py
+++ b/bot/exts/valentines/whoisvalentine.py
@@ -6,47 +6,44 @@ from random import choice
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/valentine_facts.json"), "r", encoding="utf8") as file:
- FACTS = json.load(file)
+FACTS = json.loads(Path("bot/resources/valentines/valentine_facts.json").read_text("utf8"))
class ValentineFacts(commands.Cog):
"""A Cog for displaying facts about Saint Valentine."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('whoisvalentine', 'saint_valentine'))
+ @commands.command(aliases=("whoisvalentine", "saint_valentine"))
async def who_is_valentine(self, ctx: commands.Context) -> None:
"""Displays info about Saint Valentine."""
embed = discord.Embed(
title="Who is Saint Valentine?",
- description=FACTS['whois'],
+ description=FACTS["whois"],
color=Colours.pink
)
embed.set_thumbnail(
- url='https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_'
- 'facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg'
+ url="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_"
+ "facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg"
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
@commands.command()
async def valentine_fact(self, ctx: commands.Context) -> None:
"""Shows a random fact about Valentine's Day."""
embed = discord.Embed(
- title=choice(FACTS['titles']),
- description=choice(FACTS['text']),
+ title=choice(FACTS["titles"]),
+ description=choice(FACTS["text"]),
color=Colours.pink
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Who is Valentine Cog load."""
- bot.add_cog(ValentineFacts(bot))
+def setup(bot: Bot) -> None:
+ """Load the Who is Valentine Cog."""
+ bot.add_cog(ValentineFacts())
diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json
index a4225eb1..94082849 100644
--- a/bot/resources/evergreen/trivia_quiz.json
+++ b/bot/resources/evergreen/trivia_quiz.json
@@ -44,6 +44,24 @@
],
"question": "What was the first game Yoshi appeared in?",
"answer": "Super Mario World"
+ },
+ {
+ "id": 6,
+ "hints": [
+ "They were used alternatively to playing cards.",
+ "They generally have handdrawn nature images on them."
+ ],
+ "question": "What did Nintendo make before video games and toys?",
+ "answer": "Hanafuda, Hanafuda cards"
+ },
+ {
+ "id": 7,
+ "hints": [
+ "Before being Nintendo's main competitor in home gaming, they were successful in arcades.",
+ "Their first console was called the Master System."
+ ],
+ "question": "Who was Nintendo's biggest competitor in 1990?",
+ "answer": "Sega"
}
],
"general": [
@@ -269,5 +287,322 @@
"answer": "1492",
"info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas"
}
+ ],
+ "math": [
+ {
+ "id": 201,
+ "question": "What is the highest power of a biquadratic polynomial?",
+ "answer": "4, four"
+ },
+ {
+ "id": 202,
+ "question": "What is the formula for surface area of a sphere?",
+ "answer": "4pir^2, 4πr^2"
+ },
+ {
+ "id": 203,
+ "question": "Which theorem states that hypotenuse^2 = base^2 + height^2?",
+ "answer": "Pythagorean's, Pythagorean's theorem"
+ },
+ {
+ "id": 204,
+ "question": "Which trigonometric function is defined as hypotenuse/opposite?",
+ "answer": "cosecant, cosec, csc"
+ },
+ {
+ "id": 205,
+ "question": "Does the harmonic series converge or diverge?",
+ "answer": "diverge"
+ },
+ {
+ "id": 206,
+ "question": "How many quadrants are there in a cartesian plane?",
+ "answer": "4, four"
+ },
+ {
+ "id": 207,
+ "question": "What is the (0,0) coordinate in a cartesian plane termed as?",
+ "answer": "origin"
+ },
+ {
+ "id": 208,
+ "question": "What's the following formula that finds the area of a triangle called?",
+ "img_url": "https://wikimedia.org/api/rest_v1/media/math/render/png/d22b8566e8187542966e8d166e72e93746a1a6fc",
+ "answer": "Heron's formula, Heron"
+ },
+ {
+ "id": 209,
+ "dynamic_id": 201,
+ "question": "Solve the following system of linear equations (format your answer like this <x> & <y>):\n{}x + {}y = {},\n{}x + {}y = {}",
+ "answer": "{} & {}"
+ },
+ {
+ "id": 210,
+ "dynamic_id": 202,
+ "question": "What's {} + {} mod {} congruent to?",
+ "answer": "{}"
+ },
+ {
+ "id": 211,
+ "question": "What is the bottom number on a fraction called?",
+ "answer": "denominator"
+ },
+ {
+ "id": 212,
+ "dynamic_id": 203,
+ "question": "How many vertices are on a {}gonal prism?",
+ "answer": "{}"
+ },
+ {
+ "id": 213,
+ "question": "What is the term used to describe two triangles that have equal corresponding sides and angle measures?",
+ "answer": "congruent"
+ },
+ {
+ "id": 214,
+ "question": "⅓πr^2h is the volume of which 3 dimensional figure?",
+ "answer": "cone"
+ },
+ {
+ "id": 215,
+ "dynamic_id": 204,
+ "question": "Find the square root of -{}.",
+ "answer": "{}i"
+ },
+ {
+ "id": 216,
+ "question": "In set builder notation, {p/q | q ≠ 0, p & q ∈ Z} represents what?",
+ "answer": "Rational Numbers"
+ },
+ {
+ "id": 217,
+ "question": "What is the natural log of -1 (use i for imaginary number)?",
+ "answer": "pi*i, pii, πi"
+ },
+ {
+ "id": 218,
+ "question": "When is the *inaugural* World Maths Day (format your answer in MM/DD)?",
+ "answer": "03/13"
+ },
+ {
+ "id": 219,
+ "question": "As the Fibonacci sequence extends to infinity, what's the ratio of each number `n` and its preceding number `n-1` approaching?",
+ "answer": "Golden Ratio"
+ },
+ {
+ "id": 220,
+ "question": "0, 1, 1, 2, 3, 5, 8, 13, 21, 34 are numbers of which sequence?",
+ "answer": "Fibonacci"
+ },
+ {
+ "id": 221,
+ "question": "Prime numbers only have __ factors.",
+ "answer": "2, two"
+ },
+ {
+ "id": 222,
+ "question": "In probability, the ________ ______ of an experiment or random trial is the set of all possible outcomes of it.",
+ "answer": "sample space"
+ },
+ {
+ "id": 223,
+ "question": "In statistics, what does this formula represent?",
+ "img_url": "https://www.statisticshowto.com/wp-content/uploads/2013/11/sample-standard-deviation.jpg",
+ "answer": "sample standard deviation, standard deviation of a sample"
+ },
+ {
+ "id": 224,
+ "question": "\"Hexakosioihexekontahexaphobia\" is the fear of which number?",
+ "answer": "666"
+ },
+ {
+ "id": 225,
+ "question": "A matrix multiplied by its inverse matrix equals...",
+ "answer": "the identity matrix, identity matrix"
+ },
+ {
+ "id": 226,
+ "dynamic_id": 205,
+ "question": "BASE TWO QUESTION: Calculate {:b} {} {:b}",
+ "answer": "{:b}"
+ },
+ {
+ "id": 227,
+ "question": "What is the only number in the entire number system which can be spelled with the same number of letters as itself?",
+ "answer": "4, four"
+
+ },
+ {
+ "id": 228,
+ "question": "1/100th of a second is also termed as what?",
+ "answer": "a jiffy, jiffy"
+ },
+ {
+ "id": 229,
+ "question": "What is this triangle called?",
+ "img_url": "https://cdn.askpython.com/wp-content/uploads/2020/07/Pascals-triangle.png",
+ "answer": "Pascal's triangle, Pascal"
+ },
+ {
+ "id": 230,
+ "question": "6a^2 is the surface area of which 3 dimensional figure?",
+ "answer": "cube"
+ }
+ ],
+ "science": [
+ {
+ "id": 301,
+ "question": "The three main components of a normal atom are: protons, neutrons, and...",
+ "answer": "electrons"
+ },
+ {
+ "id": 302,
+ "question": "As of 2021, how many elements are there in the Periodic Table?",
+ "answer": "118"
+ },
+ {
+ "id": 303,
+ "question": "What is the universal force discovered by Newton that causes objects with mass to attract each other called?",
+ "answer": "gravity"
+ },
+ {
+ "id": 304,
+ "question": "What do you call an organism composed of only one cell?",
+ "answer": "unicellular, single-celled"
+ },
+ {
+ "id": 305,
+ "question": "The Heisenberg's Uncertainty Principle states that the position and ________ of a quantum object can't be both exactly measured at the same time.",
+ "answer": "velocity, momentum"
+ },
+ {
+ "id": 306,
+ "question": "A ____________ reaction is the one wherein an atom or a set of atoms is/are replaced by another atom or a set of atoms",
+ "answer": "displacement, exchange"
+ },
+ {
+ "id": 307,
+ "question": "What is the process by which green plants and certain other organisms transform light energy into chemical energy?",
+ "answer": "photosynthesis"
+ },
+ {
+ "id": 308,
+ "dynamic_id": 301,
+ "question": "What is the {} planet of our Solar System?",
+ "answer": "{}"
+ },
+ {
+ "id": 309,
+ "dynamic_id": 302,
+ "question": "In the biological taxonomic hierarchy, what is placed directly above {}?",
+ "answer": "{}"
+ },
+ {
+ "id": 310,
+ "dynamic_id": 303,
+ "question": "How does one describe the unit {} in SI base units?\n**IMPORTANT:** enclose answer in backticks, use \\* for multiplication, ^ for exponentiation, and place your base units in this order: m - kg - s - A",
+ "img_url": "https://i.imgur.com/NRzU6tf.png",
+ "answer": "`{}`"
+ },
+ {
+ "id": 311,
+ "question": "How does one call the direct phase transition from gas to solid?",
+ "answer": "deposition"
+ },
+ {
+ "id": 312,
+ "question": "What is the intermolecular force caused by temporary and induced dipoles?",
+ "answer": "LDF, London dispersion, London dispersion force"
+ },
+ {
+ "id": 313,
+ "question": "What is the force that causes objects to float in fluids called?",
+ "answer": "buoyancy"
+ },
+ {
+ "id": 314,
+ "question": "About how many neurons are in the human brain? (A. 1 billion, B. 10 billion, C. 100 billion, D. 300 billion)",
+ "answer": "C, 100 billion, 100 bil"
+ },
+ {
+ "id": 315,
+ "question": "What is the name of our galaxy group in which the Milky Way resides?",
+ "answer": "Local Group"
+ },
+ {
+ "id": 316,
+ "question": "Which cell organelle is nicknamed \"the powerhouse of the cell\"?",
+ "answer": "mitochondria"
+ },
+ {
+ "id": 317,
+ "question": "Which vascular tissue transports water and minerals from the roots to the rest of a plant?",
+ "answer": "the xylem, xylem"
+ },
+ {
+ "id": 318,
+ "question": "Who discovered the theories of relativity?",
+ "answer": "Albert Einstein, Einstein"
+ },
+ {
+ "id": 319,
+ "question": "In particle physics, the hypothetical isolated elementary particle with only one magnetic pole is termed as...",
+ "answer": "magnetic monopole"
+ },
+ {
+ "id": 320,
+ "question": "How does one describe a chemical reaction wherein heat is released?",
+ "answer": "exothermic"
+ },
+ {
+ "id": 321,
+ "question": "What range of frequency are the average human ears capable of hearing (A. 10Hz-10kHz, B. 20Hz-20kHz, C. 20Hz-2000Hz, D. 10kHz-20kHz)?",
+ "answer": "B, 20Hz-20kHz"
+ },
+ {
+ "id": 322,
+ "question": "What is the process used to separate substances with different polarity in a mixture, using a stationary and mobile phase?",
+ "answer": "chromatography"
+ },
+ {
+ "id": 323,
+ "question": "Which law states that the current through a conductor between two points is directly proportional to the voltage across the two points?",
+ "answer": "Ohm's law"
+ },
+ {
+ "id": 324,
+ "question": "The type of rock that is formed by the accumulation or deposition of mineral or organic particles at the Earth's surface, followed by cementation, is called...",
+ "answer": "sedimentary, sedimentary rock"
+ },
+ {
+ "id": 325,
+ "question": "Is the Richter scale (common earthquake scale) linear or logarithmic?",
+ "answer": "logarithmic"
+ },
+ {
+ "id": 326,
+ "question": "What type of image is formed by a convex mirror?",
+ "answer": "virtual image, virtual"
+ },
+ {
+ "id": 327,
+ "question": "How does one call the branch of physics that deals with the study of mechanical waves in gases, liquids, and solids including topics such as vibration, sound, ultrasound and infrasound",
+ "answer": "acoustics"
+ },
+ {
+ "id": 328,
+ "question": "Which law states that the global entropy in a closed system can only increase?",
+ "answer": "second law, second law of thermodynamics"
+ },
+ {
+ "id": 329,
+ "question": "Which particle is emitted during the beta decay of a radioactive element?",
+ "answer": "an electron, the electron, electron"
+ },
+ {
+ "id": 330,
+ "question": "When DNA is unzipped, two strands are formed. What are they called (separate both answers by the word \"and\")?",
+ "answer": "leading and lagging, leading strand and lagging strand"
+ }
]
}
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 35ef0a7b..bef12d25 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -3,7 +3,7 @@ import contextlib
import re
import string
from datetime import datetime
-from typing import Iterable, List
+from typing import Iterable, List, Optional
import discord
from discord.ext.commands import BadArgument, Context
@@ -31,8 +31,13 @@ def resolve_current_month() -> Month:
async def disambiguate(
- ctx: Context, entries: List[str], *, timeout: float = 30,
- entries_per_page: int = 20, empty: bool = False, embed: discord.Embed = None
+ ctx: Context,
+ entries: List[str],
+ *,
+ timeout: float = 30,
+ entries_per_page: int = 20,
+ empty: bool = False,
+ embed: Optional[discord.Embed] = None
) -> str:
"""
Has the user choose between multiple entries in case one could not be chosen automatically.
@@ -43,25 +48,29 @@ async def disambiguate(
or if the user makes an invalid choice.
"""
if len(entries) == 0:
- raise BadArgument('No matches found.')
+ raise BadArgument("No matches found.")
if len(entries) == 1:
return entries[0]
- choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1))
+ choices = (f"{index}: {entry}" for index, entry in enumerate(entries, start=1))
def check(message: discord.Message) -> bool:
- return (message.content.isdigit()
- and message.author == ctx.author
- and message.channel == ctx.channel)
+ return (
+ message.content.isdecimal()
+ and message.author == ctx.author
+ and message.channel == ctx.channel
+ )
try:
if embed is None:
embed = discord.Embed()
- coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout)
- coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=entries_per_page,
- empty=empty, max_size=6000, timeout=9000)
+ coro1 = ctx.bot.wait_for("message", check=check, timeout=timeout)
+ coro2 = LinePaginator.paginate(
+ choices, ctx, embed=embed, max_lines=entries_per_page,
+ empty=empty, max_size=6000, timeout=9000
+ )
# wait_for timeout will go to except instead of the wait_for thing as I expected
futures = [asyncio.ensure_future(coro1), asyncio.ensure_future(coro2)]
@@ -74,7 +83,7 @@ async def disambiguate(
if result is None:
for coro in pending:
coro.cancel()
- raise BadArgument('Canceled.')
+ raise BadArgument("Canceled.")
# Pagination was not initiated, only one page
if result.author == ctx.bot.user:
@@ -85,19 +94,19 @@ async def disambiguate(
for coro in pending:
coro.cancel()
except asyncio.TimeoutError:
- raise BadArgument('Timed out.')
+ raise BadArgument("Timed out.")
- # Guaranteed to not error because of isdigit() in check
+ # Guaranteed to not error because of isdecimal() in check
index = int(result.content)
try:
return entries[index - 1]
except IndexError:
- raise BadArgument('Invalid choice.')
+ raise BadArgument("Invalid choice.")
def replace_many(
- sentence: str, replacements: dict, *, ignore_case: bool = False, match_case: bool = False
+ sentence: str, replacements: dict, *, ignore_case: bool = False, match_case: bool = False
) -> str:
"""
Replaces multiple substrings in a string given a mapping of strings.
@@ -139,7 +148,7 @@ def replace_many(
return replacement
# Clean punctuation from word so string methods work
- cleaned_word = word.translate(str.maketrans('', '', string.punctuation))
+ cleaned_word = word.translate(str.maketrans("", "", string.punctuation))
if cleaned_word.isupper():
return replacement.upper()
elif cleaned_word[0].isupper():
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
index 9dd4dde0..c06b6870 100644
--- a/bot/utils/checks.py
+++ b/bot/utils/checks.py
@@ -92,8 +92,10 @@ def in_whitelist_check(
def with_role_check(ctx: Context, *role_ids: int) -> bool:
"""Returns True if the user has any one of the roles in role_ids."""
if not ctx.guild: # Return False in a DM
- log.trace(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
- "This command is restricted by the with_role decorator. Rejecting request.")
+ log.trace(
+ f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
+ "This command is restricted by the with_role decorator. Rejecting request."
+ )
return False
for role in ctx.author.roles:
@@ -101,22 +103,28 @@ def with_role_check(ctx: Context, *role_ids: int) -> bool:
log.trace(f"{ctx.author} has the '{role.name}' role, and passes the check.")
return True
- log.trace(f"{ctx.author} does not have the required role to use "
- f"the '{ctx.command.name}' command, so the request is rejected.")
+ log.trace(
+ f"{ctx.author} does not have the required role to use "
+ f"the '{ctx.command.name}' command, so the request is rejected."
+ )
return False
def without_role_check(ctx: Context, *role_ids: int) -> bool:
"""Returns True if the user does not have any of the roles in role_ids."""
if not ctx.guild: # Return False in a DM
- log.trace(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
- "This command is restricted by the without_role decorator. Rejecting request.")
+ log.trace(
+ f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
+ "This command is restricted by the without_role decorator. Rejecting request."
+ )
return False
author_roles = [role.id for role in ctx.author.roles]
check = all(role not in author_roles for role in role_ids)
- log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The result of the without_role check was {check}.")
+ log.trace(
+ f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the without_role check was {check}."
+ )
return check
@@ -154,8 +162,10 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy
#
# If the `before_invoke` detail is ever a problem then I can quickly just swap over.
if not isinstance(command, Command):
- raise TypeError('Decorator `cooldown_with_role_bypass` must be applied after the command decorator. '
- 'This means it has to be above the command decorator in the code.')
+ raise TypeError(
+ "Decorator `cooldown_with_role_bypass` must be applied after the command decorator. "
+ "This means it has to be above the command decorator in the code."
+ )
command._before_invoke = predicate
diff --git a/bot/utils/converters.py b/bot/utils/converters.py
index 27804170..fe2c980c 100644
--- a/bot/utils/converters.py
+++ b/bot/utils/converters.py
@@ -1,12 +1,14 @@
+from datetime import datetime
+from typing import Tuple, Union
+
import discord
-from discord.ext.commands import BadArgument, Context
-from discord.ext.commands.converter import Converter, MessageConverter
+from discord.ext import commands
-class WrappedMessageConverter(MessageConverter):
+class WrappedMessageConverter(commands.MessageConverter):
"""A converter that handles embed-suppressed links like <http://example.com>."""
- async def convert(self, ctx: discord.ext.commands.Context, argument: str) -> discord.Message:
+ async def convert(self, ctx: commands.Context, argument: str) -> discord.Message:
"""Wrap the commands.MessageConverter to handle <> delimited message links."""
# It's possible to wrap a message in [<>] as well, and it's supported because its easy
if argument.startswith("[") and argument.endswith("]"):
@@ -17,11 +19,78 @@ class WrappedMessageConverter(MessageConverter):
return await super().convert(ctx, argument)
-class Subreddit(Converter):
+class CoordinateConverter(commands.Converter):
+ """Converter for Coordinates."""
+
+ @staticmethod
+ async def convert(ctx: commands.Context, coordinate: str) -> Tuple[int, int]:
+ """Take in a coordinate string and turn it into an (x, y) tuple."""
+ if len(coordinate) not in (2, 3):
+ raise commands.BadArgument("Invalid co-ordinate provided.")
+
+ coordinate = coordinate.lower()
+ if coordinate[0].isalpha():
+ digit = coordinate[1:]
+ letter = coordinate[0]
+ else:
+ digit = coordinate[:-1]
+ letter = coordinate[-1]
+
+ if not digit.isdecimal():
+ raise commands.BadArgument
+
+ x = ord(letter) - ord("a")
+ y = int(digit) - 1
+
+ if (not 0 <= x <= 9) or (not 0 <= y <= 9):
+ raise commands.BadArgument
+ return x, y
+
+
+SourceType = Union[commands.Command, commands.Cog]
+
+
+class SourceConverter(commands.Converter):
+ """Convert an argument into a command or cog."""
+
+ @staticmethod
+ async def convert(ctx: commands.Context, argument: str) -> SourceType:
+ """Convert argument into source object."""
+ cog = ctx.bot.get_cog(argument)
+ if cog:
+ return cog
+
+ cmd = ctx.bot.get_command(argument)
+ if cmd:
+ return cmd
+
+ raise commands.BadArgument(
+ f"Unable to convert `{argument}` to valid command or Cog."
+ )
+
+
+class DateConverter(commands.Converter):
+ """Parse SOL or earth date (in format YYYY-MM-DD) into `int` or `datetime`. When invalid input, raise error."""
+
+ @staticmethod
+ async def convert(ctx: commands.Context, argument: str) -> Union[int, datetime]:
+ """Parse date (SOL or earth) into `datetime` or `int`. When invalid value, raise error."""
+ if argument.isdecimal():
+ return int(argument)
+ try:
+ date = datetime.strptime(argument, "%Y-%m-%d")
+ except ValueError:
+ raise commands.BadArgument(
+ f"Can't convert `{argument}` to `datetime` in format `YYYY-MM-DD` or `int` in SOL."
+ )
+ return date
+
+
+class Subreddit(commands.Converter):
"""Forces a string to begin with "r/" and checks if it's a valid subreddit."""
@staticmethod
- async def convert(ctx: Context, sub: str) -> str:
+ async def convert(ctx: commands.Context, sub: str) -> str:
"""
Force sub to begin with "r/" and check if it's a valid subreddit.
@@ -39,7 +108,7 @@ class Subreddit(Converter):
json = await resp.json()
if not json["data"]["children"]:
- raise BadArgument(
+ raise commands.BadArgument(
f"The subreddit `{sub}` either doesn't exist, or it has no posts."
)
diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py
index 60066dc4..c0783144 100644
--- a/bot/utils/decorators.py
+++ b/bot/utils/decorators.py
@@ -269,7 +269,7 @@ def whitelist_check(**default_kwargs: t.Container[int]) -> t.Callable[[Context],
channels.update(channel.id for channel in category.text_channels)
if channels:
- channels_str = ', '.join(f"<#{c_id}>" for c_id in channels)
+ channels_str = ", ".join(f"<#{c_id}>" for c_id in channels)
message = f"Sorry, but you may only use this command within {channels_str}."
else:
message = "Sorry, but you may not use this command."
diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py
index 459588a1..cd491c4b 100644
--- a/bot/utils/extensions.py
+++ b/bot/utils/extensions.py
@@ -35,8 +35,8 @@ def walk_extensions() -> Iterator[str]:
async def invoke_help_command(ctx: Context) -> None:
"""Invoke the help command or default help command if help extensions is not loaded."""
- if 'bot.exts.evergreen.help' in ctx.bot.extensions:
- help_command = ctx.bot.get_command('help')
+ if "bot.exts.evergreen.help" in ctx.bot.extensions:
+ help_command = ctx.bot.get_command("help")
await ctx.invoke(help_command, ctx.command.qualified_name)
return
await ctx.send_help(ctx.command)
diff --git a/bot/utils/halloween/spookifications.py b/bot/utils/halloween/spookifications.py
index 11f69850..f69dd6fd 100644
--- a/bot/utils/halloween/spookifications.py
+++ b/bot/utils/halloween/spookifications.py
@@ -13,16 +13,16 @@ def inversion(im: Image) -> Image:
Returns an inverted image when supplied with an Image object.
"""
- im = im.convert('RGB')
+ im = im.convert("RGB")
inv = ImageOps.invert(im)
return inv
def pentagram(im: Image) -> Image:
"""Adds pentagram to the image."""
- im = im.convert('RGB')
+ im = im.convert("RGB")
wt, ht = im.size
- penta = Image.open('bot/resources/halloween/bloody-pentagram.png')
+ penta = Image.open("bot/resources/halloween/bloody-pentagram.png")
penta = penta.resize((wt, ht))
im.paste(penta, (0, 0), penta)
return im
@@ -35,9 +35,9 @@ def bat(im: Image) -> Image:
The bat silhoutte is of a size at least one-fifths that of the original image and may be rotated
up to 90 degrees anti-clockwise.
"""
- im = im.convert('RGB')
+ im = im.convert("RGB")
wt, ht = im.size
- bat = Image.open('bot/resources/halloween/bat-clipart.png')
+ bat = Image.open("bot/resources/halloween/bat-clipart.png")
bat_size = randint(wt//10, wt//7)
rot = randint(0, 90)
bat = bat.resize((bat_size, bat_size))
diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py
index 917275c0..d9c0862a 100644
--- a/bot/utils/pagination.py
+++ b/bot/utils/pagination.py
@@ -27,7 +27,14 @@ class EmptyPaginatorEmbed(Exception):
class LinePaginator(Paginator):
"""A class that aids in paginating code blocks for Discord messages."""
- def __init__(self, prefix: str = '```', suffix: str = '```', max_size: int = 2000, max_lines: int = None):
+ def __init__(
+ self,
+ prefix: str = '```',
+ suffix: str = '```',
+ max_size: int = 2000,
+ max_lines: Optional[int] = None,
+ linesep: str = "\n"
+ ):
"""
Overrides the Paginator.__init__ from inside discord.ext.commands.
@@ -36,16 +43,20 @@ class LinePaginator(Paginator):
`max_size` and `max_lines` denote the maximum amount of codepoints and lines
allowed per page.
"""
- self.prefix = prefix
- self.suffix = suffix
- self.max_size = max_size - len(suffix)
+ super().__init__(
+ prefix,
+ suffix,
+ max_size - len(suffix),
+ linesep
+ )
+
self.max_lines = max_lines
self._current_page = [prefix]
self._linecount = 0
self._count = len(prefix) + 1 # prefix + newline
self._pages = []
- def add_line(self, line: str = '', *, empty: bool = False) -> None:
+ def add_line(self, line: str = "", *, empty: bool = False) -> None:
"""
Adds a line to the current page.
@@ -57,7 +68,7 @@ class LinePaginator(Paginator):
If `empty` is True, an empty line will be placed after the a given `line`.
"""
if len(line) > self.max_size - len(self.prefix) - 2:
- raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
+ raise RuntimeError("Line exceeds maximum page size %s" % (self.max_size - len(self.prefix) - 2))
if self.max_lines is not None:
if self._linecount >= self.max_lines:
@@ -72,7 +83,7 @@ class LinePaginator(Paginator):
self._current_page.append(line)
if empty:
- self._current_page.append('')
+ self._current_page.append("")
self._count += 1
@classmethod
@@ -80,7 +91,7 @@ class LinePaginator(Paginator):
prefix: str = "", suffix: str = "", max_lines: Optional[int] = None,
max_size: int = 500, empty: bool = True, restrict_to_user: User = None,
timeout: int = 300, footer_text: str = None, url: str = None,
- exception_on_empty_embed: bool = False):
+ exception_on_empty_embed: bool = False) -> None:
"""
Use a paginator and set of reactions to provide pagination over a set of lines.
@@ -158,7 +169,8 @@ class LinePaginator(Paginator):
log.trace(f"Setting embed url to '{url}'")
log.debug("There's less than two pages, so we won't paginate - sending single page on its own")
- return await ctx.send(embed=embed)
+ await ctx.send(embed=embed)
+ return
else:
if footer_text:
embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
@@ -283,7 +295,7 @@ class ImagePaginator(Paginator):
self.images = []
self._pages = []
- def add_line(self, line: str = '', *, empty: bool = False) -> None:
+ def add_line(self, line: str = "", *, empty: bool = False) -> None:
"""
Adds a line to each page, usually just 1 line in this context.
@@ -303,7 +315,7 @@ class ImagePaginator(Paginator):
@classmethod
async def paginate(cls, pages: List[Tuple[str, str]], ctx: Context, embed: Embed,
prefix: str = "", suffix: str = "", timeout: int = 300,
- exception_on_empty_embed: bool = False):
+ exception_on_empty_embed: bool = False) -> None:
"""
Use a paginator and set of reactions to provide pagination over a set of title/image pairs.
@@ -353,7 +365,8 @@ class ImagePaginator(Paginator):
embed.set_image(url=image)
if len(paginator.pages) <= 1:
- return await ctx.send(embed=embed)
+ await ctx.send(embed=embed)
+ return
embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
message = await ctx.send(embed=embed)
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 00000000..f70d0328
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1337 @@
+[[package]]
+name = "aiodns"
+version = "2.0.0"
+description = "Simple DNS resolver for asyncio"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycares = ">=3.0.0"
+
+[[package]]
+name = "aiohttp"
+version = "3.7.4.post0"
+description = "Async http client/server framework (asyncio)"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+async-timeout = ">=3.0,<4.0"
+attrs = ">=17.3.0"
+chardet = ">=2.0,<5.0"
+multidict = ">=4.5,<7.0"
+typing-extensions = ">=3.6.5"
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["aiodns", "brotlipy", "cchardet"]
+
+[[package]]
+name = "aioredis"
+version = "1.3.1"
+description = "asyncio (PEP 3156) Redis support"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+async-timeout = "*"
+hiredis = "*"
+
+[[package]]
+name = "appdirs"
+version = "1.4.4"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "arrow"
+version = "0.17.0"
+description = "Better dates & times for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+python-dateutil = ">=2.7.0"
+
+[[package]]
+name = "async-rediscache"
+version = "0.1.4"
+description = "An easy to use asynchronous Redis cache"
+category = "main"
+optional = false
+python-versions = "~=3.7"
+
+[package.dependencies]
+aioredis = ">=1"
+fakeredis = {version = ">=1.3.1", optional = true, markers = "extra == \"fakeredis\""}
+
+[package.extras]
+fakeredis = ["fakeredis (>=1.3.1)"]
+
+[[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 = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.9.3"
+description = "Screen-scraping library"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""}
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "certifi"
+version = "2020.12.5"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "cffi"
+version = "1.14.5"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.2.0"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "chardet"
+version = "4.0.0"
+description = "Universal encoding detector for Python 2 and 3"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "cycler"
+version = "0.10.0"
+description = "Composable style cycles"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+
+[[package]]
+name = "discord.py"
+version = "1.7.2"
+description = "A Python wrapper for the Discord API"
+category = "main"
+optional = false
+python-versions = ">=3.5.3"
+
+[package.dependencies]
+aiohttp = ">=3.6.0,<3.8.0"
+
+[package.extras]
+docs = ["sphinx (==3.0.3)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"]
+voice = ["PyNaCl (>=1.3.0,<1.5)"]
+
+[[package]]
+name = "distlib"
+version = "0.3.1"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "emojis"
+version = "0.6.0"
+description = "Emojis for Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "fakeredis"
+version = "1.5.0"
+description = "Fake implementation of redis API for testing purposes."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+redis = "<3.6.0"
+six = ">=1.12"
+sortedcontainers = "*"
+
+[package.extras]
+aioredis = ["aioredis"]
+lua = ["lupa"]
+
+[[package]]
+name = "filelock"
+version = "3.0.12"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "flake8"
+version = "3.9.2"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
+
+[[package]]
+name = "flake8-annotations"
+version = "2.6.2"
+description = "Flake8 Type Annotation Checks"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1,<4.0.0"
+
+[package.dependencies]
+flake8 = ">=3.7,<4.0"
+
+[[package]]
+name = "flake8-bugbear"
+version = "20.11.1"
+description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+flake8 = ">=3.0.0"
+
+[package.extras]
+dev = ["coverage", "black", "hypothesis", "hypothesmith"]
+
+[[package]]
+name = "flake8-docstrings"
+version = "1.6.0"
+description = "Extension for flake8 which uses pydocstyle to check docstrings"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+flake8 = ">=3"
+pydocstyle = ">=2.1"
+
+[[package]]
+name = "flake8-import-order"
+version = "0.18.1"
+description = "Flake8 and pylama plugin that checks the ordering of import statements."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycodestyle = "*"
+
+[[package]]
+name = "flake8-polyfill"
+version = "1.0.2"
+description = "Polyfill package for Flake8 plugins"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+flake8 = "*"
+
+[[package]]
+name = "flake8-string-format"
+version = "0.3.0"
+description = "string format checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+flake8 = "*"
+
+[[package]]
+name = "flake8-tidy-imports"
+version = "4.3.0"
+description = "A flake8 plugin that helps you write tidier imports."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+flake8 = ">=3.0,<3.2.0 || >3.2.0,<4"
+
+[[package]]
+name = "flake8-todo"
+version = "0.7"
+description = "TODO notes checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycodestyle = ">=2.0.0,<3.0.0"
+
+[[package]]
+name = "fuzzywuzzy"
+version = "0.18.0"
+description = "Fuzzy string matching in python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+speedup = ["python-levenshtein (>=0.12)"]
+
+[[package]]
+name = "hiredis"
+version = "2.0.0"
+description = "Python wrapper for hiredis"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "identify"
+version = "2.2.4"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.extras]
+license = ["editdistance-s"]
+
+[[package]]
+name = "idna"
+version = "3.1"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.4"
+
+[[package]]
+name = "kiwisolver"
+version = "1.3.1"
+description = "A fast implementation of the Cassowary constraint solver"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "matplotlib"
+version = "3.4.2"
+description = "Python plotting package"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cycler = ">=0.10"
+kiwisolver = ">=1.0.1"
+numpy = ">=1.16"
+pillow = ">=6.2.0"
+pyparsing = ">=2.2.1"
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mslex"
+version = "0.3.0"
+description = "shlex for windows"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "multidict"
+version = "5.1.0"
+description = "multidict implementation"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "nodeenv"
+version = "1.6.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "numpy"
+version = "1.20.3"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "pep8-naming"
+version = "0.11.1"
+description = "Check PEP-8 naming conventions, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+flake8-polyfill = ">=1.0.2,<2"
+
+[[package]]
+name = "pillow"
+version = "8.2.0"
+description = "Python Imaging Library (Fork)"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pre-commit"
+version = "2.12.1"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
+[[package]]
+name = "psutil"
+version = "5.8.0"
+description = "Cross-platform lib for process and system monitoring in Python."
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
+
+[[package]]
+name = "pycares"
+version = "4.0.0"
+description = "Python interface for c-ares"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+cffi = ">=1.5.0"
+
+[package.extras]
+idna = ["idna (>=2.1)"]
+
+[[package]]
+name = "pycodestyle"
+version = "2.7.0"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pycparser"
+version = "2.20"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pydocstyle"
+version = "6.0.0"
+description = "Python docstring style checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+snowballstemmer = "*"
+
+[[package]]
+name = "pyflakes"
+version = "2.3.1"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.1"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-dotenv"
+version = "0.15.0"
+description = "Add .env support to your django/flask apps in development and deployments"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "pytz"
+version = "2019.3"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyyaml"
+version = "5.4.1"
+description = "YAML parser and emitter for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[[package]]
+name = "redis"
+version = "3.5.3"
+description = "Python client for Redis key-value store"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+hiredis = ["hiredis (>=0.1.3)"]
+
+[[package]]
+name = "sentry-sdk"
+version = "0.20.3"
+description = "Python client for Sentry (https://sentry.io)"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+certifi = "*"
+urllib3 = ">=1.10.0"
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.5)"]
+beam = ["apache-beam (>=2.12)"]
+bottle = ["bottle (>=0.12.13)"]
+celery = ["celery (>=3)"]
+chalice = ["chalice (>=1.16.0)"]
+django = ["django (>=1.8)"]
+falcon = ["falcon (>=1.4)"]
+flask = ["flask (>=0.11)", "blinker (>=1.1)"]
+pure_eval = ["pure-eval", "executing", "asttokens"]
+pyspark = ["pyspark (>=2.4.4)"]
+rq = ["rq (>=0.6)"]
+sanic = ["sanic (>=0.8)"]
+sqlalchemy = ["sqlalchemy (>=1.2)"]
+tornado = ["tornado (>=5)"]
+
+[[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.*"
+
+[[package]]
+name = "snowballstemmer"
+version = "2.1.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "sortedcontainers"
+version = "2.3.0"
+description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "soupsieve"
+version = "2.2.1"
+description = "A modern CSS selector implementation for Beautiful Soup."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "taskipy"
+version = "1.7.0"
+description = "tasks runner for python projects"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+mslex = ">=0.3.0,<0.4.0"
+psutil = ">=5.7.2,<6.0.0"
+toml = ">=0.10.0,<0.11.0"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "typing-extensions"
+version = "3.10.0.0"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "urllib3"
+version = "1.26.4"
+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.*, <4"
+
+[package.extras]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+brotli = ["brotlipy (>=0.6.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.4.6"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.dependencies]
+appdirs = ">=1.4.3,<2"
+distlib = ">=0.3.1,<1"
+filelock = ">=3.0.0,<4"
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
+
+[[package]]
+name = "yarl"
+version = "1.6.3"
+description = "Yet another URL library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.9"
+content-hash = "d0003b8cc4caac9d6eb0c14e4c4085191907d7fa0803888eddae4259446eada7"
+
+[metadata.files]
+aiodns = [
+ {file = "aiodns-2.0.0-py2.py3-none-any.whl", hash = "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de"},
+ {file = "aiodns-2.0.0.tar.gz", hash = "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d"},
+]
+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"},
+]
+aioredis = [
+ {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"},
+ {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"},
+]
+appdirs = [
+ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+arrow = [
+ {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"},
+ {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"},
+]
+async-rediscache = [
+ {file = "async-rediscache-0.1.4.tar.gz", hash = "sha256:6be8a657d724ccbcfb1946d29a80c3478c5f9ecd2f78a0a26d2f4013a622258f"},
+ {file = "async_rediscache-0.1.4-py3-none-any.whl", hash = "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af"},
+]
+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"},
+]
+attrs = [
+ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+]
+beautifulsoup4 = [
+ {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
+ {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
+ {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
+]
+certifi = [
+ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
+ {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+]
+cffi = [
+ {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"},
+ {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"},
+ {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"},
+ {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"},
+ {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"},
+ {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"},
+ {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"},
+ {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"},
+ {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"},
+ {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"},
+ {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"},
+ {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"},
+ {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"},
+ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
+ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
+ {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
+ {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
+ {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
+ {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
+ {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
+ {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
+ {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
+ {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
+ {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
+ {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
+ {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
+ {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
+ {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
+ {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
+ {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
+ {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
+ {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
+ {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
+ {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
+ {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
+ {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
+ {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
+]
+cfgv = [
+ {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
+ {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
+]
+chardet = [
+ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
+ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
+]
+cycler = [
+ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"},
+ {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"},
+]
+"discord.py" = [
+ {file = "discord.py-1.7.2-py3-none-any.whl", hash = "sha256:f179db299c949a8cf0a12c1b1b94d0da9a18e088857154d93ae5ab1d807ec61d"},
+ {file = "discord.py-1.7.2.tar.gz", hash = "sha256:114e76cd27362fb919abf7f001a2dbdc77c9a67cff74ed6a89aecd6582ee298e"},
+]
+distlib = [
+ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
+ {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
+]
+emojis = [
+ {file = "emojis-0.6.0-py3-none-any.whl", hash = "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c"},
+ {file = "emojis-0.6.0.tar.gz", hash = "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec"},
+]
+fakeredis = [
+ {file = "fakeredis-1.5.0-py3-none-any.whl", hash = "sha256:e0416e4941cecd3089b0d901e60c8dc3c944f6384f5e29e2261c0d3c5fa99669"},
+ {file = "fakeredis-1.5.0.tar.gz", hash = "sha256:1ac0cef767c37f51718874a33afb5413e69d132988cb6a80c6e6dbeddf8c7623"},
+]
+filelock = [
+ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
+ {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
+]
+flake8 = [
+ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
+ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
+]
+flake8-annotations = [
+ {file = "flake8-annotations-2.6.2.tar.gz", hash = "sha256:0d6cd2e770b5095f09689c9d84cc054c51b929c41a68969ea1beb4b825cac515"},
+ {file = "flake8_annotations-2.6.2-py3-none-any.whl", hash = "sha256:d10c4638231f8a50c0a597c4efce42bd7b7d85df4f620a0ddaca526138936a4f"},
+]
+flake8-bugbear = [
+ {file = "flake8-bugbear-20.11.1.tar.gz", hash = "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538"},
+ {file = "flake8_bugbear-20.11.1-py36.py37.py38-none-any.whl", hash = "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"},
+]
+flake8-docstrings = [
+ {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"},
+ {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"},
+]
+flake8-import-order = [
+ {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"},
+ {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"},
+]
+flake8-polyfill = [
+ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"},
+ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
+]
+flake8-string-format = [
+ {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"},
+ {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"},
+]
+flake8-tidy-imports = [
+ {file = "flake8-tidy-imports-4.3.0.tar.gz", hash = "sha256:e66d46f58ed108f36da920e7781a728dc2d8e4f9269e7e764274105700c0a90c"},
+ {file = "flake8_tidy_imports-4.3.0-py3-none-any.whl", hash = "sha256:d6e64cb565ca9474d13d5cb3f838b8deafb5fed15906998d4a674daf55bd6d89"},
+]
+flake8-todo = [
+ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"},
+]
+fuzzywuzzy = [
+ {file = "fuzzywuzzy-0.18.0-py2.py3-none-any.whl", hash = "sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993"},
+ {file = "fuzzywuzzy-0.18.0.tar.gz", hash = "sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8"},
+]
+hiredis = [
+ {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"},
+ {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"},
+ {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"},
+ {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"},
+ {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"},
+ {file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"},
+ {file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"},
+ {file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"},
+ {file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"},
+ {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"},
+ {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"},
+ {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"},
+ {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"},
+ {file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"},
+ {file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"},
+ {file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"},
+ {file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"},
+ {file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"},
+ {file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"},
+ {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"},
+ {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"},
+ {file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"},
+ {file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"},
+ {file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"},
+ {file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"},
+ {file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"},
+ {file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"},
+ {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"},
+ {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"},
+ {file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"},
+ {file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"},
+ {file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"},
+ {file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"},
+ {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"},
+ {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"},
+ {file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"},
+ {file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"},
+ {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"},
+ {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"},
+ {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"},
+ {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"},
+]
+identify = [
+ {file = "identify-2.2.4-py2.py3-none-any.whl", hash = "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8"},
+ {file = "identify-2.2.4.tar.gz", hash = "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e"},
+]
+idna = [
+ {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"},
+ {file = "idna-3.1.tar.gz", hash = "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"},
+]
+kiwisolver = [
+ {file = "kiwisolver-1.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"},
+ {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0"},
+ {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21"},
+ {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05"},
+ {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b"},
+ {file = "kiwisolver-1.3.1-cp36-cp36m-win32.whl", hash = "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9"},
+ {file = "kiwisolver-1.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc"},
+ {file = "kiwisolver-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-win32.whl", hash = "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81"},
+ {file = "kiwisolver-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-win32.whl", hash = "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030"},
+ {file = "kiwisolver-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6"},
+ {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d"},
+ {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3"},
+ {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6"},
+ {file = "kiwisolver-1.3.1.tar.gz", hash = "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248"},
+]
+matplotlib = [
+ {file = "matplotlib-3.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c541ee5a3287efe066bbe358320853cf4916bc14c00c38f8f3d8d75275a405a9"},
+ {file = "matplotlib-3.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3a5c18dbd2c7c366da26a4ad1462fe3e03a577b39e3b503bbcf482b9cdac093c"},
+ {file = "matplotlib-3.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a9d8cb5329df13e0cdaa14b3b43f47b5e593ec637f13f14db75bb16e46178b05"},
+ {file = "matplotlib-3.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:7ad19f3fb6145b9eb41c08e7cbb9f8e10b91291396bee21e9ce761bb78df63ec"},
+ {file = "matplotlib-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:7a58f3d8fe8fac3be522c79d921c9b86e090a59637cb88e3bc51298d7a2c862a"},
+ {file = "matplotlib-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6382bc6e2d7e481bcd977eb131c31dee96e0fb4f9177d15ec6fb976d3b9ace1a"},
+ {file = "matplotlib-3.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a6a44f27aabe720ec4fd485061e8a35784c2b9ffa6363ad546316dfc9cea04e"},
+ {file = "matplotlib-3.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1c1779f7ab7d8bdb7d4c605e6ffaa0614b3e80f1e3c8ccf7b9269a22dbc5986b"},
+ {file = "matplotlib-3.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5826f56055b9b1c80fef82e326097e34dc4af8c7249226b7dd63095a686177d1"},
+ {file = "matplotlib-3.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0bea5ec5c28d49020e5d7923c2725b837e60bc8be99d3164af410eb4b4c827da"},
+ {file = "matplotlib-3.4.2-cp38-cp38-win32.whl", hash = "sha256:6475d0209024a77f869163ec3657c47fed35d9b6ed8bccba8aa0f0099fbbdaa8"},
+ {file = "matplotlib-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:21b31057bbc5e75b08e70a43cefc4c0b2c2f1b1a850f4a0f7af044eb4163086c"},
+ {file = "matplotlib-3.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b26535b9de85326e6958cdef720ecd10bcf74a3f4371bf9a7e5b2e659c17e153"},
+ {file = "matplotlib-3.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:32fa638cc10886885d1ca3d409d4473d6a22f7ceecd11322150961a70fab66dd"},
+ {file = "matplotlib-3.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:956c8849b134b4a343598305a3ca1bdd3094f01f5efc8afccdebeffe6b315247"},
+ {file = "matplotlib-3.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:85f191bb03cb1a7b04b5c2cca4792bef94df06ef473bc49e2818105671766fee"},
+ {file = "matplotlib-3.4.2-cp39-cp39-win32.whl", hash = "sha256:b1d5a2cedf5de05567c441b3a8c2651fbde56df08b82640e7f06c8cd91e201f6"},
+ {file = "matplotlib-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:df815378a754a7edd4559f8c51fc7064f779a74013644a7f5ac7a0c31f875866"},
+ {file = "matplotlib-3.4.2.tar.gz", hash = "sha256:d8d994cefdff9aaba45166eb3de4f5211adb4accac85cbf97137e98f26ea0219"},
+]
+mccabe = [
+ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+mslex = [
+ {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"},
+ {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"},
+]
+multidict = [
+ {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"},
+ {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"},
+ {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"},
+ {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"},
+ {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"},
+ {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"},
+ {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"},
+ {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"},
+ {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"},
+ {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"},
+ {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"},
+ {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"},
+ {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"},
+ {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"},
+ {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"},
+ {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"},
+ {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"},
+ {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"},
+ {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"},
+ {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"},
+ {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"},
+ {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"},
+ {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"},
+ {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"},
+ {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"},
+ {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"},
+ {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"},
+ {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"},
+ {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"},
+ {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"},
+ {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"},
+ {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"},
+ {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"},
+ {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"},
+ {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"},
+ {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"},
+ {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"},
+]
+nodeenv = [
+ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
+ {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
+]
+numpy = [
+ {file = "numpy-1.20.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8"},
+ {file = "numpy-1.20.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8"},
+ {file = "numpy-1.20.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a"},
+ {file = "numpy-1.20.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16"},
+ {file = "numpy-1.20.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2"},
+ {file = "numpy-1.20.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2"},
+ {file = "numpy-1.20.3-cp37-cp37m-win32.whl", hash = "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6"},
+ {file = "numpy-1.20.3-cp37-cp37m-win_amd64.whl", hash = "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43"},
+ {file = "numpy-1.20.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17"},
+ {file = "numpy-1.20.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b"},
+ {file = "numpy-1.20.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f"},
+ {file = "numpy-1.20.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4"},
+ {file = "numpy-1.20.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a"},
+ {file = "numpy-1.20.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65"},
+ {file = "numpy-1.20.3-cp38-cp38-win32.whl", hash = "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48"},
+ {file = "numpy-1.20.3-cp38-cp38-win_amd64.whl", hash = "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010"},
+ {file = "numpy-1.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb"},
+ {file = "numpy-1.20.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df"},
+ {file = "numpy-1.20.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400"},
+ {file = "numpy-1.20.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f"},
+ {file = "numpy-1.20.3-cp39-cp39-win32.whl", hash = "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd"},
+ {file = "numpy-1.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4"},
+ {file = "numpy-1.20.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9"},
+ {file = "numpy-1.20.3.zip", hash = "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69"},
+]
+pep8-naming = [
+ {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"},
+ {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"},
+]
+pillow = [
+ {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"},
+ {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"},
+ {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"},
+ {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"},
+ {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"},
+ {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"},
+ {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"},
+ {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"},
+ {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"},
+ {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"},
+ {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"},
+ {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"},
+ {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"},
+ {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"},
+ {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"},
+ {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"},
+ {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"},
+ {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"},
+ {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"},
+ {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"},
+ {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"},
+ {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"},
+ {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"},
+ {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"},
+ {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"},
+ {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"},
+ {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"},
+ {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"},
+ {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"},
+ {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"},
+ {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"},
+ {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"},
+ {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"},
+]
+pre-commit = [
+ {file = "pre_commit-2.12.1-py2.py3-none-any.whl", hash = "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712"},
+ {file = "pre_commit-2.12.1.tar.gz", hash = "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9"},
+]
+psutil = [
+ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
+ {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
+ {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
+ {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
+ {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
+ {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
+ {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
+ {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
+ {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
+ {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
+ {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
+ {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
+ {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
+ {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
+ {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
+ {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
+ {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
+ {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
+ {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
+ {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
+ {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
+ {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
+ {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
+ {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
+ {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
+ {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
+ {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
+ {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
+]
+pycares = [
+ {file = "pycares-4.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db5a533111a3cfd481e7e4fb2bf8bef69f4fa100339803e0504dd5aecafb96a5"},
+ {file = "pycares-4.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fdff88393c25016f417770d82678423fc7a56995abb2df3d2a1e55725db6977d"},
+ {file = "pycares-4.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0aa97f900a7ffb259be77d640006585e2a907b0cd4edeee0e85cf16605995d5a"},
+ {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a34b0e3e693dceb60b8a1169668d606c75cb100ceba0a2df53c234a0eb067fbc"},
+ {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7661d6bbd51a337e7373cb356efa8be9b4655fda484e068f9455e939aec8d54e"},
+ {file = "pycares-4.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:57315b8eb8fdbc56b3ad4932bc4b17132bb7c7fd2bd590f7fb84b6b522098aa9"},
+ {file = "pycares-4.0.0-cp36-cp36m-win32.whl", hash = "sha256:dca9dc58845a9d083f302732a3130c68ded845ad5d463865d464e53c75a3dd45"},
+ {file = "pycares-4.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c95c964d5dd307e104b44b193095c67bb6b10c9eda1ffe7d44ab7a9e84c476d9"},
+ {file = "pycares-4.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26e67e4f81c80a5955dcf6193f3d9bee3c491fc0056299b383b84d792252fba4"},
+ {file = "pycares-4.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd3011ffd5e1ad55880f7256791dbab9c43ebeda260474a968f19cd0319e1aef"},
+ {file = "pycares-4.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b959dd5921d207d759d421eece1b60416df33a7f862465739d5f2c363c2f523"},
+ {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6f258c1b74c048a9501a25f732f11b401564005e5e3c18f1ca6cad0c3dc0fb19"},
+ {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b17ef48729786e62b574c6431f675f4cb02b27691b49e7428a605a50cd59c072"},
+ {file = "pycares-4.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:82b3259cb590ddd107a6d2dc52da2a2e9a986bf242e893d58c786af2f8191047"},
+ {file = "pycares-4.0.0-cp37-cp37m-win32.whl", hash = "sha256:4876fc790ae32832ae270c4a010a1a77e12ddf8d8e6ad70ad0b0a9d506c985f7"},
+ {file = "pycares-4.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f60c04c5561b1ddf85ca4e626943cc09d7fb684e1adb22abb632095415a40fd7"},
+ {file = "pycares-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:615406013cdcd1b445e5d1a551d276c6200b3abe77e534f8a7f7e1551208d14f"},
+ {file = "pycares-4.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6580aef5d1b29a88c3d72fe73c691eacfd454f86e74d3fdd18f4bad8e8def98b"},
+ {file = "pycares-4.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ebb3ba0485f66cae8eed7ce3e9ed6f2c0bfd5e7319d5d0fbbb511064f17e1d4"},
+ {file = "pycares-4.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c5362b7690ca481440f6b98395ac6df06aa50518ccb183c560464d1e5e2ab5d4"},
+ {file = "pycares-4.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:eb60be66accc9a9ea1018b591a1f5800cba83491d07e9acc8c56bc6e6607ab54"},
+ {file = "pycares-4.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:44896d6e191a6b5a914dbe3aa7c748481bf6ad19a9df33c1e76f8f2dc33fc8f0"},
+ {file = "pycares-4.0.0-cp38-cp38-win32.whl", hash = "sha256:09b28fc7bc2cc05f7f69bf1636ddf46086e0a1837b62961e2092fcb40477320d"},
+ {file = "pycares-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4a5081e232c1d181883dcac4675807f3a6cf33911c4173fbea00c0523687ed4"},
+ {file = "pycares-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:103353577a6266a53e71bfee4cf83825f1401fefa60f0fb8bdec35f13be6a5f2"},
+ {file = "pycares-4.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ad6caf580ee69806fc6534be93ddbb6e99bf94296d79ab351c37b2992b17abfd"},
+ {file = "pycares-4.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3d5e50c95849f6905d2a9dbf02ed03f82580173e3c5604a39e2ad054185631f1"},
+ {file = "pycares-4.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:53bc4f181b19576499b02cea4b45391e8dcbe30abd4cd01492f66bfc15615a13"},
+ {file = "pycares-4.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d52f9c725d2a826d5ffa37681eb07ffb996bfe21788590ef257664a3898fc0b5"},
+ {file = "pycares-4.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3c7fb8d34ee11971c39acfaf98d0fac66725385ccef3bfe1b174c92b210e1aa4"},
+ {file = "pycares-4.0.0-cp39-cp39-win32.whl", hash = "sha256:e9773e07684a55f54657df05237267611a77b294ec3bacb5f851c4ffca38a465"},
+ {file = "pycares-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:38e54037f36c149146ff15f17a4a963fbdd0f9871d4a21cd94ff9f368140f57e"},
+ {file = "pycares-4.0.0.tar.gz", hash = "sha256:d0154fc5753b088758fbec9bc137e1b24bb84fc0c6a09725c8bac25a342311cd"},
+]
+pycodestyle = [
+ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+ {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+]
+pycparser = [
+ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
+ {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
+]
+pydocstyle = [
+ {file = "pydocstyle-6.0.0-py3-none-any.whl", hash = "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d"},
+ {file = "pydocstyle-6.0.0.tar.gz", hash = "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f"},
+]
+pyflakes = [
+ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
+ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
+]
+pyparsing = [
+ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+python-dateutil = [
+ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
+ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
+]
+python-dotenv = [
+ {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"},
+ {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"},
+]
+pytz = [
+ {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
+ {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
+]
+pyyaml = [
+ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
+ {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
+ {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
+ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
+ {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
+ {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+ {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
+ {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
+ {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
+ {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
+ {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
+ {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+ {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
+ {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
+ {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
+ {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
+ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
+]
+redis = [
+ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
+ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
+]
+sentry-sdk = [
+ {file = "sentry-sdk-0.20.3.tar.gz", hash = "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237"},
+ {file = "sentry_sdk-0.20.3-py2.py3-none-any.whl", hash = "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"},
+]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+snowballstemmer = [
+ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
+ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
+]
+sortedcontainers = [
+ {file = "sortedcontainers-2.3.0-py2.py3-none-any.whl", hash = "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f"},
+ {file = "sortedcontainers-2.3.0.tar.gz", hash = "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"},
+]
+soupsieve = [
+ {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"},
+ {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"},
+]
+taskipy = [
+ {file = "taskipy-1.7.0-py3-none-any.whl", hash = "sha256:9e284c10898e9dee01a3e72220b94b192b1daa0f560271503a6df1da53d03844"},
+ {file = "taskipy-1.7.0.tar.gz", hash = "sha256:960e480b1004971e76454ecd1a0484e640744a30073a1069894a311467f85ed8"},
+]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+typing-extensions = [
+ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
+ {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
+ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
+]
+urllib3 = [
+ {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
+ {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
+]
+virtualenv = [
+ {file = "virtualenv-20.4.6-py2.py3-none-any.whl", hash = "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543"},
+ {file = "virtualenv-20.4.6.tar.gz", hash = "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4"},
+]
+yarl = [
+ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"},
+ {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"},
+ {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"},
+ {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"},
+ {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"},
+ {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"},
+ {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"},
+ {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"},
+ {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"},
+ {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"},
+ {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"},
+ {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"},
+ {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"},
+ {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"},
+ {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"},
+ {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"},
+ {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"},
+ {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"},
+ {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"},
+ {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"},
+ {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"},
+ {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"},
+ {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"},
+ {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"},
+ {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"},
+ {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"},
+ {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"},
+ {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"},
+ {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"},
+ {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"},
+ {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"},
+ {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"},
+ {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"},
+ {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"},
+ {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"},
+ {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"},
+ {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"},
+]
diff --git a/Pipfile b/pyproject.toml
index f6118f1a..2528511e 100644
--- a/Pipfile
+++ b/pyproject.toml
@@ -1,9 +1,12 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
+[tool.poetry]
+name = "sir-lancebot"
+version = "0.1.0"
+description = "A Discord bot designed as a fun and beginner-friendly learning environment for writing bot features and learning open-source."
+authors = ["Python Discord <[email protected]>"]
+license = "MIT"
-[packages]
+[tool.poetry.dependencies]
+python = "^3.9"
aiodns = "~=2.0"
arrow = "~=0.14"
beautifulsoup4 = "~=4.8"
@@ -12,12 +15,12 @@ pillow = "~=8.1"
pytz = "~=2019.2"
sentry-sdk = "~=0.19"
PyYAML = "~=5.4"
-"discord.py" = "~=1.5.1"
+"discord.py" = "~=1.7.2"
async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"}
emojis = "~=0.6.0"
matplotlib = "~=3.4.1"
-[dev-packages]
+[tool.poetry.dev-dependencies]
flake8 = "~=3.8"
flake8-annotations = "~=2.3"
flake8-bugbear = "~=20.1"
@@ -28,11 +31,14 @@ flake8-tidy-imports = "~=4.1"
flake8-todo = "~=0.7"
pep8-naming = "~=0.11"
pre-commit = "~=2.1"
+taskipy = "^1.6.0"
+python-dotenv = "^0.15.0"
-[requires]
-python_version = "3.8"
-
-[scripts]
+[tool.taskipy.tasks]
start = "python -m bot"
lint = "pre-commit run --all-files"
precommit = "pre-commit install"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"