aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Rohan Reddy Alleti <[email protected]>2023-05-17 12:23:29 +0530
committerGravatar GitHub <[email protected]>2023-05-17 12:23:29 +0530
commit7876744fedab9fbb22a160c8950ee22262570270 (patch)
treef7189b5f3e9c0947d71f86461f151e50092f6e2c
parentnit (diff)
parentBump sentry-sdk from 1.22.2 to 1.23.0 (#1277) (diff)
Merge branch 'main' into undeprecate-bookmark
-rw-r--r--.github/workflows/lint.yaml23
-rw-r--r--.pre-commit-config.yaml29
-rw-r--r--bot/__init__.py2
-rw-r--r--bot/__main__.py10
-rw-r--r--bot/bot.py3
-rw-r--r--bot/constants.py165
-rw-r--r--bot/exts/avatar_modification/_effects.py8
-rw-r--r--bot/exts/avatar_modification/avatar_modify.py26
-rw-r--r--bot/exts/core/error_handler.py14
-rw-r--r--bot/exts/core/extensions.py16
-rw-r--r--bot/exts/core/help.py30
-rw-r--r--bot/exts/core/internal_eval/_helpers.py10
-rw-r--r--bot/exts/core/internal_eval/_internal_eval.py3
-rw-r--r--bot/exts/core/source.py18
-rw-r--r--bot/exts/events/hacktoberfest/hacktober_issue_finder.py (renamed from bot/exts/events/hacktoberfest/hacktober-issue-finder.py)11
-rw-r--r--bot/exts/events/hacktoberfest/hacktoberstats.py43
-rw-r--r--bot/exts/events/hacktoberfest/timeleft.py14
-rw-r--r--bot/exts/events/trivianight/_game.py23
-rw-r--r--bot/exts/events/trivianight/_questions.py12
-rw-r--r--bot/exts/events/trivianight/trivianight.py18
-rw-r--r--bot/exts/fun/anagram.py2
-rw-r--r--bot/exts/fun/battleship.py20
-rw-r--r--bot/exts/fun/catify.py30
-rw-r--r--bot/exts/fun/connect_four.py29
-rw-r--r--bot/exts/fun/duck_game.py14
-rw-r--r--bot/exts/fun/game.py38
-rw-r--r--bot/exts/fun/hangman.py9
-rw-r--r--bot/exts/fun/latex.py19
-rw-r--r--bot/exts/fun/madlibs.py2
-rw-r--r--bot/exts/fun/minesweeper.py11
-rw-r--r--bot/exts/fun/movie.py19
-rw-r--r--bot/exts/fun/quack.py6
-rw-r--r--bot/exts/fun/snakes/__init__.py3
-rw-r--r--bot/exts/fun/snakes/_snakes_cog.py113
-rw-r--r--bot/exts/fun/snakes/_utils.py46
-rw-r--r--bot/exts/fun/space.py34
-rw-r--r--bot/exts/fun/tic_tac_toe.py25
-rw-r--r--bot/exts/fun/trivia_quiz.py14
-rw-r--r--bot/exts/fun/uwu.py6
-rw-r--r--bot/exts/fun/wonder_twins.py4
-rw-r--r--bot/exts/fun/xkcd.py5
-rw-r--r--bot/exts/holidays/easter/bunny_name_generator.py8
-rw-r--r--bot/exts/holidays/easter/earth_photos.py8
-rw-r--r--bot/exts/holidays/easter/easter_riddle.py2
-rw-r--r--bot/exts/holidays/easter/egg_decorating.py15
-rw-r--r--bot/exts/holidays/easter/egghead_quiz.py13
-rw-r--r--bot/exts/holidays/halloween/candy_collection.py15
-rw-r--r--bot/exts/holidays/halloween/eight_ball.py (renamed from bot/exts/holidays/halloween/8ball.py)0
-rw-r--r--bot/exts/holidays/halloween/monstersurvey.py4
-rw-r--r--bot/exts/holidays/halloween/scarymovie.py7
-rw-r--r--bot/exts/holidays/halloween/spookygif.py5
-rw-r--r--bot/exts/holidays/halloween/spookynamerate.py19
-rw-r--r--bot/exts/holidays/hanukkah/hanukkah_embed.py19
-rw-r--r--bot/exts/holidays/holidayreact.py4
-rw-r--r--bot/exts/holidays/pride/pride_anthem.py15
-rw-r--r--bot/exts/holidays/pride/pride_facts.py13
-rw-r--r--bot/exts/holidays/pride/pride_leader.py3
-rw-r--r--bot/exts/holidays/valentines/be_my_valentine.py20
-rw-r--r--bot/exts/holidays/valentines/lovecalculator.py10
-rw-r--r--bot/exts/holidays/valentines/myvalenstate.py5
-rw-r--r--bot/exts/holidays/valentines/valentine_zodiac.py8
-rw-r--r--bot/exts/utilities/bookmark.py7
-rw-r--r--bot/exts/utilities/challenges.py10
-rw-r--r--bot/exts/utilities/cheatsheet.py5
-rw-r--r--bot/exts/utilities/colour.py13
-rw-r--r--bot/exts/utilities/conversationstarters.py9
-rw-r--r--bot/exts/utilities/emoji.py7
-rw-r--r--bot/exts/utilities/epoch.py13
-rw-r--r--bot/exts/utilities/githubinfo.py27
-rw-r--r--bot/exts/utilities/realpython.py10
-rw-r--r--bot/exts/utilities/reddit.py37
-rw-r--r--bot/exts/utilities/stackoverflow.py6
-rw-r--r--bot/exts/utilities/twemoji.py10
-rw-r--r--bot/exts/utilities/wikipedia.py4
-rw-r--r--bot/exts/utilities/wolfram.py24
-rw-r--r--bot/exts/utilities/wtf_python.py5
-rw-r--r--bot/utils/__init__.py19
-rw-r--r--bot/utils/checks.py12
-rw-r--r--bot/utils/converters.py9
-rw-r--r--bot/utils/decorators.py28
-rw-r--r--bot/utils/exceptions.py5
-rw-r--r--bot/utils/messages.py25
-rw-r--r--bot/utils/pagination.py47
-rw-r--r--bot/utils/randomization.py8
-rw-r--r--bot/utils/time.py13
-rw-r--r--poetry.lock797
-rw-r--r--pyproject.toml39
-rw-r--r--tox.ini26
88 files changed, 1094 insertions, 1241 deletions
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 1a5e4bff..68d35d82 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -42,25 +42,16 @@ jobs:
- name: Attempt bot setup
run: "python -m bot"
env:
- USE_FAKEREDIS: true
- IN_CI: true
+ REDIS_USE_FAKEREDIS: true
+ CLIENT_IN_CI: true
+ CLIENT_TOKEN: ""
- # We will not run `flake8` here, as we will use a separate flake8
- # action.
- name: Run pre-commit hooks
- run: SKIP=flake8 pre-commit run --all-files
+ run: SKIP=ruff pre-commit run --all-files
- # Run flake8 and have it format the linting errors in the format of
- # the GitHub Workflow command to register error annotations. This
- # means that our flake8 output is automatically added as an error
- # annotation to both the run result and in the "Files" tab of a
- # pull request.
- #
- # Format used:
- # ::error file={filename},line={line},col={col}::{message}
- - name: Run flake8
- run: "flake8 \
- --format='::error file=%(path)s,line=%(row)d,col=%(col)d::[flake8] %(code)s: %(text)s'"
+ # Run `ruff` using github formatting to enable automatic inline annotations.
+ - name: Run ruff
+ run: "ruff check --format=github ."
# Prepare the Pull Request Payload artifact. If this fails, we
# we fail silently using the `continue-on-error` option. It's
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7895d208..3fdd30bf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v2.5.0
+ rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: check-toml
@@ -8,21 +8,22 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- - repo: https://github.com/pre-commit/pygrep-hooks
- rev: v1.5.1
- hooks:
- - id: python-check-blanket-noqa
- - repo: https://github.com/pycqa/isort
- rev: 5.12.0
- hooks:
- - id: isort
- name: isort (python)
+
- repo: local
hooks:
- - id: flake8
- name: Flake8
- description: This hook runs flake8 within our project's poetry environment.
- entry: poetry run flake8
+ - id: isort
+ name: isort
+ description: This hook runs isort within our project's environment.
+ entry: poetry run isort
language: system
types: [python]
require_serial: true
+
+ - id: ruff
+ name: ruff
+ description: Run ruff linting
+ entry: poetry run ruff check --force-exclude
+ language: system
+ 'types_or': [python, pyi]
+ require_serial: true
+ args: [--fix, --exit-non-zero-on-fix]
diff --git a/bot/__init__.py b/bot/__init__.py
index 454e235e..9ae64c2e 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,6 +1,6 @@
try:
from dotenv import load_dotenv
- print("Found .env file, loading environment variables from it.")
+ print("Found .env file, loading environment variables from it.") # noqa: T201
load_dotenv(override=True)
except ModuleNotFoundError:
pass
diff --git a/bot/__main__.py b/bot/__main__.py
index d39e93bf..1b6b9f1e 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -19,11 +19,11 @@ log = logging.getLogger(__name__)
async def _create_redis_session() -> RedisSession:
"""Create and connect to a redis session."""
redis_session = RedisSession(
- host=constants.RedisConfig.host,
- port=constants.RedisConfig.port,
- password=constants.RedisConfig.password,
+ host=constants.Redis.host,
+ port=constants.Redis.port,
+ password=constants.Redis.password.get_secret_value(),
max_connections=20,
- use_fakeredis=constants.RedisConfig.use_fakeredis,
+ use_fakeredis=constants.Redis.use_fakeredis,
global_namespace="bot",
decode_responses=True,
)
@@ -81,7 +81,7 @@ async def main() -> None:
if constants.Client.in_ci:
await test_bot_in_ci(_bot)
else:
- await _bot.start(constants.Client.token)
+ await _bot.start(constants.Client.token.get_secret_value())
asyncio.run(main())
diff --git a/bot/bot.py b/bot/bot.py
index da19bdd9..0b20ac54 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -1,5 +1,4 @@
import logging
-from typing import Optional
import discord
from discord import DiscordException, Embed
@@ -26,7 +25,7 @@ class Bot(BotBase):
name = constants.Client.name
@property
- def member(self) -> Optional[discord.Member]:
+ def member(self) -> discord.Member | None:
"""Retrieves the guild member object for the bot."""
guild = self.get_guild(constants.Client.guild)
if not guild:
diff --git a/bot/constants.py b/bot/constants.py
index 7e5fa1d9..cd866a0b 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -1,11 +1,10 @@
import enum
import logging
from os import environ
-from typing import NamedTuple
+
+from pydantic import BaseSettings, SecretStr
__all__ = (
- "Branding",
- "Cats",
"Channels",
"Categories",
"Client",
@@ -13,14 +12,12 @@ __all__ = (
"Colours",
"Emojis",
"Icons",
- "Lovefest",
"Month",
"Roles",
"Tokens",
"Wolfram",
"Reddit",
- "RedisConfig",
- "RedirectOutput",
+ "Redis",
"PYTHON_PREFIX",
"MODERATION_ROLES",
"STAFF_ROLES",
@@ -36,21 +33,25 @@ log = logging.getLogger(__name__)
PYTHON_PREFIX = "!"
-class Branding:
- cycle_frequency = int(environ.get("CYCLE_FREQUENCY", 3)) # 0: never, 1: every day, 2: every other day, ...
+class EnvConfig(BaseSettings):
+ """Our default configuration for models that should load from .env files."""
+
+ class Config:
+ """Specify what .env files to load, and how to load them."""
+ env_file = ".env",
+ env_file_encoding = "utf-8"
-class Cats:
- cats = ["ᓚᘏᗢ", "ᘡᘏᗢ", "🐈", "ᓕᘏᗢ", "ᓇᘏᗢ", "ᓂᘏᗢ", "ᘣᘏᗢ", "ᕦᘏᗢ", "ᕂᘏᗢ"]
+class _Channels(EnvConfig):
+ EnvConfig.Config.env_prefix = "channels_"
-class Channels(NamedTuple):
algos_and_data_structs = 650401909852864553
bot_commands = 267659945086812160
community_meta = 267659945086812160
organisation = 551789653284356126
data_science_and_ai = 366673247892275221
- devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554))
+ devlog = 622895325144940554
dev_contrib = 635950537262759947
mod_meta = 775412552795947058
mod_tools = 775413915391098921
@@ -58,43 +59,60 @@ class Channels(NamedTuple):
off_topic_1 = 463035241142026251
off_topic_2 = 463035268514185226
python_help = 1035199133436354600
- sir_lancebot_playground = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354))
+ sir_lancebot_playground = 607247579608121354
voice_chat_0 = 412357430186344448
voice_chat_1 = 799647045886541885
staff_voice = 541638762007101470
- reddit = int(environ.get("CHANNEL_REDDIT", 458224812528238616))
+ reddit = 458224812528238616
+
+
+Channels = _Channels()
-class Categories(NamedTuple):
- help_in_use = 696958401460043776
+class _Categories(EnvConfig):
+ EnvConfig.Config.env_prefix = "categories_"
+
+ python_help_system = 691405807388196926
development = 411199786025484308
devprojects = 787641585624940544
media = 799054581991997460
staff = 364918151625965579
-codejam_categories_name = "Code Jam" # Name of the codejam team categories
+Categories = _Categories()
+
+class _Client(EnvConfig):
+ EnvConfig.Config.env_prefix = "client_"
-class Client(NamedTuple):
name = "Sir Lancebot"
- guild = int(environ.get("BOT_GUILD", 267624335836053506))
- prefix = environ.get("PREFIX", ".")
- token = environ.get("BOT_TOKEN")
- debug = environ.get("BOT_DEBUG", "true").lower() == "true"
- in_ci = environ.get("IN_CI", "false").lower() == "true"
- github_bot_repo = "https://github.com/python-discord/sir-lancebot"
+ guild = 267624335836053506
+ prefix = "."
+ token: SecretStr
+ debug = True
+ in_ci = False
+ github_repo = "https://github.com/python-discord/sir-lancebot"
# Override seasonal locks: 1 (January) to 12 (December)
- month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None
+ month_override: int | None = None
+
+Client = _Client()
+
+
+class _Logging(EnvConfig):
+ EnvConfig.Config.env_prefix = "logging_"
-class Logging(NamedTuple):
debug = Client.debug
- file_logs = environ.get("FILE_LOGS", "false").lower() == "true"
- trace_loggers = environ.get("BOT_TRACE_LOGGERS")
+ file_logs = False
+ trace_loggers = ""
+
+
+Logging = _Logging()
class Colours:
+ """Lookups for commonly used colours."""
+
blue = 0x0279FD
twitter_blue = 0x1DA1F2
bright_green = 0x01D277
@@ -129,6 +147,8 @@ class Colours:
class Emojis:
+ """Commonly used emojis."""
+
cross_mark = "\u274C"
check = "\u2611"
envelope = "\U0001F4E8"
@@ -201,6 +221,8 @@ class Emojis:
class Icons:
+ """URLs to commonly used icons."""
+
questionmark = "https://cdn.discordapp.com/emojis/512367613339369475.png"
bookmark = (
"https://images-ext-2.discordapp.net/external/zl4oDwcmxUILY7sD9ZWE2fU5R7n6QcxEmPYSE5eddbg/"
@@ -208,11 +230,9 @@ class Icons:
)
-class Lovefest:
- role_id = int(environ.get("LOVEFEST_ROLE_ID", 542431903886606399))
-
-
class Month(enum.IntEnum):
+ """Month of the year lookup. Used for in_month checks."""
+
JANUARY = 1
FEBRUARY = 2
MARCH = 3
@@ -236,57 +256,72 @@ if Client.month_override is not None:
Month(Client.month_override)
-class Roles(NamedTuple):
+class _Roles(EnvConfig):
+
+ EnvConfig.Config.env_prefix = "roles_"
+
owners = 267627879762755584
- admins = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896))
+ admins = 267628507062992896
moderation_team = 267629731250176001
- helpers = int(environ.get("ROLE_HELPERS", 267630620367257601))
+ helpers = 267630620367257601
core_developers = 587606783669829632
- everyone = int(environ.get("BOT_GUILD", 267624335836053506))
+ everyone = Client.guild
+
+ lovefest = 542431903886606399
+
+Roles = _Roles()
-class Tokens(NamedTuple):
- giphy = environ.get("GIPHY_TOKEN")
- aoc_session_cookie = environ.get("AOC_SESSION_COOKIE")
- omdb = environ.get("OMDB_API_KEY")
- youtube = environ.get("YOUTUBE_API_KEY")
- tmdb = environ.get("TMDB_API_KEY")
- nasa = environ.get("NASA_API_KEY")
- igdb_client_id = environ.get("IGDB_CLIENT_ID")
- igdb_client_secret = environ.get("IGDB_CLIENT_SECRET")
- github = environ.get("GITHUB_TOKEN")
- unsplash_access_key = environ.get("UNSPLASH_KEY")
+class _Tokens(EnvConfig):
+ EnvConfig.Config.env_prefix = "tokens_"
-class Wolfram(NamedTuple):
- user_limit_day = int(environ.get("WOLFRAM_USER_LIMIT_DAY", 10))
- guild_limit_day = int(environ.get("WOLFRAM_GUILD_LIMIT_DAY", 67))
- key = environ.get("WOLFRAM_API_KEY")
+ giphy: SecretStr = ""
+ youtube: SecretStr = ""
+ tmdb: SecretStr = ""
+ nasa: SecretStr = ""
+ igdb_client_id: SecretStr = ""
+ igdb_client_secret: SecretStr = ""
+ github: SecretStr = ""
+ unsplash: SecretStr = ""
-class RedisConfig(NamedTuple):
- host = environ.get("REDIS_HOST", "redis.default.svc.cluster.local")
- port = environ.get("REDIS_PORT", 6379)
- password = environ.get("REDIS_PASSWORD")
- use_fakeredis = environ.get("USE_FAKEREDIS", "false").lower() == "true"
+Tokens = _Tokens()
-class Source:
- github = "https://github.com/python-discord/sir-lancebot"
- github_avatar_url = "https://avatars1.githubusercontent.com/u/9919"
+class _Wolfram(EnvConfig):
+ EnvConfig.Config.env_prefix = "wolfram_"
+ user_limit_day = 10
+ guild_limit_day = 67
+ key: SecretStr = ""
-class RedirectOutput:
- delete_delay: int = 10
+Wolfram = _Wolfram()
-class Reddit:
+class _Redis(EnvConfig):
+ EnvConfig.Config.env_prefix = "redis_"
+
+ host = "redis.default.svc.cluster.local"
+ port = 6379
+ password: SecretStr = ""
+ use_fakeredis = False
+
+
+Redis = _Redis()
+
+
+class _Reddit(EnvConfig):
+ EnvConfig.Config.env_prefix = "reddit_"
+
subreddits = ["r/Python"]
- client_id = environ.get("REDDIT_CLIENT_ID")
- secret = environ.get("REDDIT_SECRET")
- webhook = int(environ.get("REDDIT_WEBHOOK", 635408384794951680))
+ client_id: SecretStr = ""
+ secret: SecretStr = ""
+ webhook = 635408384794951680
+
+Reddit = _Reddit()
# Default role combinations
MODERATION_ROLES = {Roles.moderation_team, Roles.admins, Roles.owners}
diff --git a/bot/exts/avatar_modification/_effects.py b/bot/exts/avatar_modification/_effects.py
index f1c2e6d1..fcc62ebb 100644
--- a/bot/exts/avatar_modification/_effects.py
+++ b/bot/exts/avatar_modification/_effects.py
@@ -1,8 +1,8 @@
import math
import random
+from collections.abc import Callable
from io import BytesIO
from pathlib import Path
-from typing import Callable, Optional
import discord
from PIL import Image, ImageDraw, ImageOps
@@ -55,7 +55,7 @@ class PfpEffects:
@staticmethod
def crop_avatar_circle(avatar: Image.Image) -> Image.Image:
- """This crops the avatar given into a circle."""
+ """Crop the avatar given into a circle."""
mask = Image.new("L", avatar.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0) + avatar.size, fill=255)
@@ -64,7 +64,7 @@ class PfpEffects:
@staticmethod
def crop_ring(ring: Image.Image, px: int) -> Image.Image:
- """This crops the given ring into a circle."""
+ """Crop the given ring into a circle."""
mask = Image.new("L", ring.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0) + ring.size, fill=255)
@@ -108,7 +108,7 @@ class PfpEffects:
return image
@staticmethod
- def easterify_effect(image: Image.Image, overlay_image: Optional[Image.Image] = None) -> Image.Image:
+ def easterify_effect(image: Image.Image, overlay_image: Image.Image | None = None) -> Image.Image:
"""
Applies the easter effect to the given image.
diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py
index 6d1f26f6..4eae269f 100644
--- a/bot/exts/avatar_modification/avatar_modify.py
+++ b/bot/exts/avatar_modification/avatar_modify.py
@@ -4,9 +4,10 @@ import logging
import math
import string
import unicodedata
+from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
-from typing import Callable, Optional, TypeVar, Union
+from typing import TypeVar
import discord
from discord.ext import commands
@@ -64,7 +65,7 @@ class AvatarModify(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- async def _fetch_user(self, user_id: int) -> Optional[discord.User]:
+ async def _fetch_user(self, user_id: int) -> discord.User | None:
"""
Fetches a user and handles errors.
@@ -120,7 +121,7 @@ class AvatarModify(commands.Cog):
await ctx.send(embed=embed, file=file)
@avatar_modify.command(name="reverse", root_aliases=("reverse",))
- async def reverse(self, ctx: commands.Context, *, text: Optional[str]) -> None:
+ async def reverse(self, ctx: commands.Context, *, text: str | None) -> None:
"""
Reverses the sent text.
@@ -157,9 +158,9 @@ class AvatarModify(commands.Cog):
await ctx.send(embed=embed, file=file)
@avatar_modify.command(aliases=("easterify",), root_aliases=("easterify", "avatareasterify"))
- async def avatareasterify(self, ctx: commands.Context, *colours: Union[discord.Colour, str]) -> None:
+ async def avatareasterify(self, ctx: commands.Context, *colours: discord.Colour | str) -> None:
"""
- This "Easterifies" the user's avatar.
+ Easterify the user's avatar.
Given colours will produce a personalised egg in the corner, similar to the egg_decorate command.
If colours are not given, a nice little chocolate bunny will sit in the corner.
@@ -168,13 +169,14 @@ class AvatarModify(commands.Cog):
"""
async def send(*args, **kwargs) -> str:
"""
- This replaces the original ctx.send.
+ Replace the original ctx.send.
When invoking the egg decorating command, the egg itself doesn't print to to the channel.
- Returns the message content so that if any errors occur, the error message can be output.
+ Return the message content so that if any errors occur, the error message can be output.
"""
if args:
return args[0]
+ return None
async with ctx.typing():
user = await self._fetch_user(ctx.author.id)
@@ -248,11 +250,11 @@ class AvatarModify(commands.Cog):
)
async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None:
"""
- This surrounds an avatar with a border of a specified LGBT flag.
+ Surround an avatar with a border of a specified LGBT flag.
- This defaults to the LGBT rainbow flag if none is given.
+ Default to the LGBT rainbow flag if none is given.
The amount of pixels can be given which determines the thickness of the flag border.
- This has a maximum of 512px and defaults to a 64px border.
+ A maximum of 512px is enforced, defaults to a 64px border.
The full image is 1024x1024.
"""
option = option.lower()
@@ -272,7 +274,7 @@ class AvatarModify(commands.Cog):
@prideavatar.command()
async def flags(self, ctx: commands.Context) -> None:
- """This lists the flags that can be used with the prideavatar command."""
+ """Lists the flags that can be used with the prideavatar command."""
choices = sorted(set(GENDER_OPTIONS.values()))
options = "• " + "\n• ".join(choices)
embed = discord.Embed(
@@ -288,7 +290,7 @@ class AvatarModify(commands.Cog):
brief="Spookify a user's avatar."
)
async def spookyavatar(self, ctx: commands.Context) -> None:
- """This "spookifies" the user's avatar, with a random *spooky* effect."""
+ """Spookify the user's avatar, with a random *spooky* effect."""
user = await self._fetch_user(ctx.author.id)
if not user:
await ctx.send(f"{Emojis.cross_mark} Could not get user info.")
diff --git a/bot/exts/core/error_handler.py b/bot/exts/core/error_handler.py
index 04ab55d7..81b923fd 100644
--- a/bot/exts/core/error_handler.py
+++ b/bot/exts/core/error_handler.py
@@ -2,14 +2,13 @@ import logging
import math
import random
from collections.abc import Iterable
-from typing import Union
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, RedirectOutput
+from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES
from bot.utils.commands import get_command_suggestions
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import APIError, MovedCommandError, UserNotPlayingError
@@ -17,6 +16,7 @@ from bot.utils.exceptions import APIError, MovedCommandError, UserNotPlayingErro
log = logging.getLogger(__name__)
+DELETE_DELAY = 10
QUESTION_MARK_ICON = "https://cdn.discordapp.com/emojis/512367613339369475.png"
@@ -35,7 +35,7 @@ class CommandErrorHandler(commands.Cog):
logging.debug("Cooldown counter reverted as the command was not used correctly.")
@staticmethod
- def error_embed(message: str, title: Union[Iterable, str] = ERROR_REPLIES) -> Embed:
+ def error_embed(message: str, title: Iterable | str = ERROR_REPLIES) -> Embed:
"""Build a basic embed with red colour and either a random error title or a title provided."""
embed = Embed(colour=Colours.soft_red)
if isinstance(title, str):
@@ -59,7 +59,7 @@ class CommandErrorHandler(commands.Cog):
error = getattr(error, "original", error)
logging.debug(
- f"Error Encountered: {type(error).__name__} - {str(error)}, "
+ f"Error Encountered: {type(error).__name__} - {error!s}, "
f"Command: {ctx.command}, "
f"Author: {ctx.author}, "
f"Channel: {ctx.channel}"
@@ -71,7 +71,7 @@ class CommandErrorHandler(commands.Cog):
await self.send_command_suggestion(ctx, ctx.invoked_with)
return
- if isinstance(error, (InChannelCheckFailure, InMonthCheckFailure)):
+ if isinstance(error, InChannelCheckFailure | InMonthCheckFailure):
await ctx.send(embed=self.error_embed(str(error), NEGATIVE_REPLIES), delete_after=7.5)
return
@@ -156,7 +156,7 @@ class CommandErrorHandler(commands.Cog):
if ctx.guild is not None:
scope.set_extra("jump_to", ctx.message.jump_url)
- log.exception(f"Unhandled command error: {str(error)}", exc_info=error)
+ log.exception(f"Unhandled command error: {error!s}", exc_info=error)
async def send_command_suggestion(self, ctx: commands.Context, command_name: str) -> None:
"""Sends user similar commands if any can be found."""
@@ -185,7 +185,7 @@ class CommandErrorHandler(commands.Cog):
e.description = "\n".join(
misspelled_content.replace(command_name, cmd, 1) for cmd in command_suggestions
)
- await ctx.send(embed=e, delete_after=RedirectOutput.delete_delay)
+ await ctx.send(embed=e, delete_after=DELETE_DELAY)
async def setup(bot: Bot) -> None:
diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py
index 1d22cf37..87ef2ed1 100644
--- a/bot/exts/core/extensions.py
+++ b/bot/exts/core/extensions.py
@@ -2,7 +2,6 @@ import functools
import logging
from collections.abc import Mapping
from enum import Enum
-from typing import Optional
from discord import Colour, Embed
from discord.ext import commands
@@ -48,7 +47,7 @@ class Extension(commands.Converter):
if argument in ctx.bot.all_extensions:
return argument
- elif (qualified_arg := f"{exts.__name__}.{argument}") in ctx.bot.all_extensions:
+ if (qualified_arg := f"{exts.__name__}.{argument}") in ctx.bot.all_extensions:
return qualified_arg
matches = []
@@ -63,10 +62,9 @@ class Extension(commands.Converter):
f":x: `{argument}` is an ambiguous extension name. "
f"Please use one of the following fully-qualified names.```\n{names}\n```"
)
- elif matches:
+ if matches:
return matches[0]
- else:
- raise commands.BadArgument(f":x: Could not find the extension `{argument}`.")
+ raise commands.BadArgument(f":x: Could not find the extension `{argument}`.")
class Extensions(commands.Cog):
@@ -86,7 +84,7 @@ class Extensions(commands.Cog):
Load extensions given their fully qualified or unqualified names.
If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded.
- """ # noqa: W605
+ """
if not extensions:
await self.bot.invoke_help_command(ctx)
return
@@ -103,7 +101,7 @@ class Extensions(commands.Cog):
Unload currently loaded extensions given their fully qualified or unqualified names.
If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded.
- """ # noqa: W605
+ """
if not extensions:
await self.bot.invoke_help_command(ctx)
return
@@ -129,7 +127,7 @@ class Extensions(commands.Cog):
If '\*' is given as the name, all currently loaded extensions will be reloaded.
If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded.
- """ # noqa: W605
+ """
if not extensions:
await self.bot.invoke_help_command(ctx)
return
@@ -220,7 +218,7 @@ class Extensions(commands.Cog):
return msg
- async def manage(self, action: Action, ext: str) -> tuple[str, Optional[str]]:
+ async def manage(self, action: Action, ext: str) -> tuple[str, str | None]:
"""Apply an action to an extension and return the status message and any error message."""
verb = action.name.lower()
error_msg = None
diff --git a/bot/exts/core/help.py b/bot/exts/core/help.py
index 30deaff4..9f96a333 100644
--- a/bot/exts/core/help.py
+++ b/bot/exts/core/help.py
@@ -3,7 +3,7 @@ import asyncio
import itertools
import logging
from contextlib import suppress
-from typing import NamedTuple, Optional, Union
+from typing import NamedTuple
from discord import Colour, Embed, HTTPException, Message, Reaction, User
from discord.ext import commands
@@ -38,7 +38,7 @@ class Cog(NamedTuple):
log = logging.getLogger(__name__)
-class HelpQueryNotFound(ValueError):
+class HelpQueryNotFoundError(ValueError):
"""
Raised when a HelpSession Query doesn't match a command or cog.
@@ -49,7 +49,7 @@ class HelpQueryNotFound(ValueError):
"""
def __init__(
- self, arg: str, possible_matches: Optional[list[str]] = None, *, parent_command: Optional[Command] = None
+ self, arg: str, possible_matches: list[str] | None = None, *, parent_command: Command | None = None
) -> None:
super().__init__(arg)
self.possible_matches = possible_matches
@@ -117,7 +117,7 @@ class HelpSession:
self._timeout_task = None
self.reset_timeout()
- def _get_query(self, query: str) -> Union[Command, Cog]:
+ def _get_query(self, query: str) -> Command | Cog | None:
"""Attempts to match the provided query with a valid command or cog."""
command = self._bot.get_command(query)
if command:
@@ -151,6 +151,7 @@ class HelpSession:
)
self._handle_not_found(query)
+ return None
def _handle_not_found(self, query: str) -> None:
"""
@@ -164,11 +165,11 @@ class HelpSession:
parent_command = self._bot.get_command(parent)
if parent_command:
- raise HelpQueryNotFound('Invalid Subcommand.', parent_command=parent_command)
+ raise HelpQueryNotFoundError("Invalid Subcommand.", parent_command=parent_command)
similar_commands = get_command_suggestions(list(self._bot.all_commands.keys()), query)
- raise HelpQueryNotFound(f'Query "{query}" not found.', similar_commands)
+ raise HelpQueryNotFoundError(f'Query "{query}" not found.', similar_commands)
async def timeout(self, seconds: int = 30) -> None:
"""Waits for a set number of seconds, then stops the help session."""
@@ -178,9 +179,8 @@ class HelpSession:
def reset_timeout(self) -> None:
"""Cancels the original timeout task and sets it again from the start."""
# cancel original if it exists
- if self._timeout_task:
- if not self._timeout_task.cancelled():
- self._timeout_task.cancel()
+ if self._timeout_task and not self._timeout_task.cancelled():
+ self._timeout_task.cancel()
# recreate the timeout task
self._timeout_task = self._bot.loop.create_task(self.timeout())
@@ -252,8 +252,7 @@ class HelpSession:
pass
return f"**{cmd.cog_name}**"
- else:
- return "**\u200bNo Category:**"
+ return "**\u200bNo Category:**"
def _get_command_params(self, cmd: Command) -> str:
"""
@@ -286,8 +285,7 @@ class HelpSession:
# if required
else:
results.append(f"<{name}>")
-
- return f"{cmd.qualified_name} {' '.join(results)}"
+ return " ".join([cmd.qualified_name, *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."""
@@ -305,7 +303,7 @@ class HelpSession:
paginator.add_line(f"*{self.description}*")
# list all children commands of the queried object
- if isinstance(self.query, (commands.GroupMixin, Cog)):
+ if isinstance(self.query, commands.GroupMixin | Cog):
await self._list_child_commands(paginator)
self._pages = paginator.pages
@@ -418,7 +416,7 @@ class HelpSession:
"""Returns an Embed with the requested page formatted within."""
embed = Embed()
- if isinstance(self.query, (commands.Command, Cog)) and page_number > 0:
+ if isinstance(self.query, commands.Command | Cog) and page_number > 0:
title = f'Command Help | "{self.query.name}"'
else:
title = self.title
@@ -518,7 +516,7 @@ class Help(DiscordCog):
"""Shows Command Help."""
try:
await HelpSession.start(ctx, *commands)
- except HelpQueryNotFound as error:
+ except HelpQueryNotFoundError as error:
# Send help message of parent command if subcommand is invalid.
if cmd := error.parent_command:
diff --git a/bot/exts/core/internal_eval/_helpers.py b/bot/exts/core/internal_eval/_helpers.py
index 5b2f8f5d..34ef7fef 100644
--- a/bot/exts/core/internal_eval/_helpers.py
+++ b/bot/exts/core/internal_eval/_helpers.py
@@ -8,7 +8,7 @@ import logging
import sys
import traceback
import types
-from typing import Any, Optional, Union
+from typing import Any
log = logging.getLogger(__name__)
@@ -120,7 +120,7 @@ class EvalContext:
log.trace(f"Updating {self._locals} with {locals_}")
self._locals.update(locals_)
- def prepare_eval(self, code: str) -> Optional[str]:
+ def prepare_eval(self, code: str) -> str | None:
"""Prepare an evaluation by processing the code and setting up the context."""
self.code = code
@@ -149,7 +149,7 @@ class EvalContext:
compiled_code = compile(self.eval_tree, filename=INTERNAL_EVAL_FRAMENAME, mode="exec")
log.trace("Executing the compiled code with the desired namespace environment")
- exec(compiled_code, self.locals) # noqa: B102,S102
+ exec(compiled_code, self.locals) # noqa: S102
log.trace("Awaiting the created evaluation wrapper coroutine.")
await self.function()
@@ -212,7 +212,7 @@ class CaptureLastExpression(ast.NodeTransformer):
self.tree = tree
self.last_node = list(ast.iter_child_nodes(tree))[-1]
- def visit_Expr(self, node: ast.Expr) -> Union[ast.Expr, ast.Assign]: # noqa: N802
+ def visit_Expr(self, node: ast.Expr) -> ast.Expr | ast.Assign: # noqa: N802
"""
Replace the Expr node that is last child node of Module with an assignment.
@@ -230,7 +230,7 @@ class CaptureLastExpression(ast.NodeTransformer):
right_hand_side = list(ast.iter_child_nodes(node))[0]
assignment = ast.Assign(
- targets=[ast.Name(id='_value_last_expression', ctx=ast.Store())],
+ targets=[ast.Name(id="_value_last_expression", ctx=ast.Store())],
value=right_hand_side,
lineno=node.lineno,
col_offset=0,
diff --git a/bot/exts/core/internal_eval/_internal_eval.py b/bot/exts/core/internal_eval/_internal_eval.py
index 2daf8ef9..f5188c1f 100644
--- a/bot/exts/core/internal_eval/_internal_eval.py
+++ b/bot/exts/core/internal_eval/_internal_eval.py
@@ -1,7 +1,6 @@
import logging
import re
import textwrap
-from typing import Optional
import discord
from discord.ext import commands
@@ -84,7 +83,7 @@ class InternalEval(commands.Cog):
return shortened_output
- async def _upload_output(self, output: str) -> Optional[str]:
+ async def _upload_output(self, output: str) -> str | None:
"""Upload `internal eval` output to our pastebin and return the url."""
data = self.shorten_output(output, max_length=MAX_LENGTH)
try:
diff --git a/bot/exts/core/source.py b/bot/exts/core/source.py
index f771eaca..7c67bcb5 100644
--- a/bot/exts/core/source.py
+++ b/bot/exts/core/source.py
@@ -1,15 +1,17 @@
import inspect
from pathlib import Path
-from typing import Optional
from discord import Embed
from discord.ext import commands
from bot.bot import Bot
-from bot.constants import Channels, Source, WHITELISTED_CHANNELS
+from bot.constants import Channels, WHITELISTED_CHANNELS
from bot.utils.converters import SourceConverter, SourceType
from bot.utils.decorators import whitelist_override
+GITHUB_BOT_URL = "https://github.com/python-discord/sir-lancebot"
+BOT_AVATAR_URL = "https://avatars1.githubusercontent.com/u/9919"
+
class BotSource(commands.Cog):
"""Displays information about the bot's source code."""
@@ -20,15 +22,15 @@ class BotSource(commands.Cog):
"""Display information and a GitHub link to the source code of a command, tag, or cog."""
if not source_item:
embed = Embed(title="Sir Lancebot's GitHub Repository")
- embed.add_field(name="Repository", value=f"[Go to GitHub]({Source.github})")
- embed.set_thumbnail(url=Source.github_avatar_url)
+ embed.add_field(name="Repository", value=f"[Go to GitHub]({GITHUB_BOT_URL})")
+ embed.set_thumbnail(url=BOT_AVATAR_URL)
await ctx.send(embed=embed)
return
embed = await self.build_embed(source_item)
await ctx.send(embed=embed)
- def get_source_link(self, source_item: SourceType) -> tuple[str, str, Optional[int]]:
+ def get_source_link(self, source_item: SourceType) -> tuple[str, str, int | None]:
"""
Build GitHub link of source item, return this link, file location and first line number.
@@ -58,11 +60,11 @@ class BotSource(commands.Cog):
file_location = Path(filename).relative_to(Path.cwd()).as_posix()
- url = f"{Source.github}/blob/main/{file_location}{lines_extension}"
+ url = f"{GITHUB_BOT_URL}/blob/main/{file_location}{lines_extension}"
return url, file_location, first_line_no or None
- async def build_embed(self, source_object: SourceType) -> Optional[Embed]:
+ async def build_embed(self, source_object: SourceType) -> Embed | None:
"""Build embed based on source object."""
url, location, first_line = self.get_source_link(source_object)
@@ -74,7 +76,7 @@ class BotSource(commands.Cog):
description = source_object.description.splitlines()[0]
embed = Embed(title=title, description=description)
- embed.set_thumbnail(url=Source.github_avatar_url)
+ embed.set_thumbnail(url=BOT_AVATAR_URL)
embed.add_field(name="Source Code", value=f"[Go to GitHub]({url})")
line_text = f":{first_line}" if first_line else ""
embed.set_footer(text=f"{location}{line_text}")
diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober_issue_finder.py
index aeffc8d7..69aa3924 100644
--- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py
+++ b/bot/exts/events/hacktoberfest/hacktober_issue_finder.py
@@ -1,7 +1,6 @@
-import datetime
import logging
import random
-from typing import Optional
+from datetime import UTC, datetime
import discord
from discord.ext import commands
@@ -18,7 +17,7 @@ REQUEST_HEADERS = {
"User-Agent": "Python Discord Hacktoberbot",
"Accept": "application / vnd.github.v3 + json"
}
-if GITHUB_TOKEN := Tokens.github:
+if GITHUB_TOKEN := Tokens.github.get_secret_value():
REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
@@ -28,9 +27,9 @@ class HacktoberIssues(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
self.cache_normal = None
- self.cache_timer_normal = datetime.datetime(1, 1, 1)
+ self.cache_timer_normal = datetime(1, 1, 1, tzinfo=UTC)
self.cache_beginner = None
- self.cache_timer_beginner = datetime.datetime(1, 1, 1)
+ self.cache_timer_beginner = datetime(1, 1, 1, tzinfo=UTC)
@in_month(Month.OCTOBER)
@commands.command()
@@ -49,7 +48,7 @@ class HacktoberIssues(commands.Cog):
embed = self.format_embed(issue)
await ctx.send(embed=embed)
- async def get_issues(self, ctx: commands.Context, option: str) -> Optional[dict]:
+ async def get_issues(self, ctx: commands.Context, option: str) -> dict | None:
"""Get a list of the python issues with the label 'hacktoberfest' from the Github api."""
if option == "beginner":
if (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_beginner).seconds <= 60:
diff --git a/bot/exts/events/hacktoberfest/hacktoberstats.py b/bot/exts/events/hacktoberfest/hacktoberstats.py
index c29e24b0..c7fd3601 100644
--- a/bot/exts/events/hacktoberfest/hacktoberstats.py
+++ b/bot/exts/events/hacktoberfest/hacktoberstats.py
@@ -2,8 +2,7 @@ import logging
import random
import re
from collections import Counter
-from datetime import datetime, timedelta
-from typing import Optional, Union
+from datetime import UTC, datetime, timedelta
from urllib.parse import quote_plus
import discord
@@ -16,7 +15,7 @@ from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
-CURRENT_YEAR = datetime.now().year # Used to construct GH API query
+CURRENT_YEAR = datetime.now(tz=UTC).year # Used to construct GH API query
PRS_FOR_SHIRT = 4 # Minimum number of PRs before a shirt is awarded
REVIEW_DAYS = 14 # number of days needed after PR can be mature
@@ -24,8 +23,8 @@ REQUEST_HEADERS = {"User-Agent": "Python Discord Hacktoberbot"}
# using repo topics API during preview period requires an accept header
GITHUB_TOPICS_ACCEPT_HEADER = {"Accept": "application/vnd.github.mercy-preview+json"}
if GITHUB_TOKEN := Tokens.github:
- REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
- GITHUB_TOPICS_ACCEPT_HEADER["Authorization"] = f"token {GITHUB_TOKEN}"
+ REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN.get_secret_value()}"
+ GITHUB_TOPICS_ACCEPT_HEADER["Authorization"] = f"token {GITHUB_TOKEN.get_secret_value()}"
GITHUB_NONEXISTENT_USER_MESSAGE = (
"The listed users cannot be searched either because the users do not exist "
@@ -185,7 +184,7 @@ class HacktoberStats(commands.Cog):
logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")
return stats_embed
- async def get_october_prs(self, github_username: str) -> Optional[list[dict]]:
+ async def get_october_prs(self, github_username: str) -> list[dict] | None:
"""
Query GitHub's API for PRs created during the month of October by github_username.
@@ -234,9 +233,8 @@ class HacktoberStats(commands.Cog):
# Ignore logging non-existent users or users we do not have permission to see
if api_message == GITHUB_NONEXISTENT_USER_MESSAGE:
log.debug(f"No GitHub user found named '{github_username}'")
- return
- else:
- log.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
+ return None
+ 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:
@@ -246,7 +244,7 @@ class HacktoberStats(commands.Cog):
logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'")
outlist = [] # list of pr information dicts that will get returned
- oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=None)
+ oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=UTC)
hackto_topics = {} # cache whether each repo has the appropriate topic (bool values)
for item in jsonresp["items"]:
shortname = self._get_shortname(item["repository_url"])
@@ -255,15 +253,14 @@ class HacktoberStats(commands.Cog):
"repo_shortname": shortname,
"created_at": datetime.strptime(
item["created_at"], "%Y-%m-%dT%H:%M:%SZ"
- ),
+ ).replace(tzinfo=UTC),
"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 self._has_label(item, ["invalid", "spam"]):
- if not await self._is_accepted(itemdict):
- continue
+ if self._has_label(item, ["invalid", "spam"]) and not await self._is_accepted(itemdict):
+ continue
# PRs before oct 3 no need to check for topics
# continue the loop if 'hacktoberfest-accepted' is labelled then
@@ -302,7 +299,7 @@ class HacktoberStats(commands.Cog):
return await resp.json()
@staticmethod
- def _has_label(pr: dict, labels: Union[list[str], str]) -> bool:
+ def _has_label(pr: dict, labels: list[str] | str) -> bool:
"""
Check if a PR has label 'labels'.
@@ -313,7 +310,7 @@ class HacktoberStats(commands.Cog):
return False
if isinstance(labels, str) and any(label["name"].casefold() == labels for label in pr["labels"]):
return True
- for item in labels:
+ for item in labels: # noqa: SIM110
if any(label["name"].casefold() == item for label in pr["labels"]):
return True
return False
@@ -350,10 +347,7 @@ class HacktoberStats(commands.Cog):
return False
# loop through reviews and check for approval
- for item in jsonresp2:
- if item.get("status") == "APPROVED":
- return True
- return False
+ return any(item.get("status") == "APPROVED" for item in jsonresp2)
@staticmethod
def _get_shortname(in_url: str) -> str:
@@ -378,8 +372,8 @@ class HacktoberStats(commands.Cog):
PRs that are accepted must either be merged, approved, or labelled
'hacktoberfest-accepted.
"""
- now = datetime.now()
- oct3 = datetime(CURRENT_YEAR, 10, 3, 23, 59, 59, tzinfo=None)
+ now = datetime.now(tz=UTC)
+ oct3 = datetime(CURRENT_YEAR, 10, 3, 23, 59, 59, tzinfo=UTC)
in_review = []
accepted = []
for pr in prs:
@@ -420,8 +414,7 @@ class HacktoberStats(commands.Cog):
"""Return "contribution" or "contributions" based on the value of n."""
if n == 1:
return "contribution"
- else:
- return "contributions"
+ return "contributions"
@staticmethod
def _author_mention_from_context(ctx: commands.Context) -> tuple[str, str]:
@@ -434,4 +427,6 @@ class HacktoberStats(commands.Cog):
async def setup(bot: Bot) -> None:
"""Load the Hacktober Stats Cog."""
+ if not Tokens.github:
+ log.warning("No GitHub token was provided. The HacktoberStats Cog won't be fully functional.")
await bot.add_cog(HacktoberStats(bot))
diff --git a/bot/exts/events/hacktoberfest/timeleft.py b/bot/exts/events/hacktoberfest/timeleft.py
index f470e932..8f46d798 100644
--- a/bot/exts/events/hacktoberfest/timeleft.py
+++ b/bot/exts/events/hacktoberfest/timeleft.py
@@ -1,5 +1,5 @@
import logging
-from datetime import datetime
+from datetime import UTC, datetime
from discord.ext import commands
@@ -9,25 +9,25 @@ log = logging.getLogger(__name__)
class TimeLeft(commands.Cog):
- """A Cog that tells you how long left until Hacktober is over!"""
+ """A Cog that tells users how long left until Hacktober is over!"""
def in_hacktober(self) -> bool:
"""Return True if the current time is within Hacktoberfest."""
_, end, start = self.load_date()
- now = datetime.utcnow()
+ now = datetime.now(tz=UTC)
return start <= now <= end
@staticmethod
def load_date() -> tuple[datetime, datetime, datetime]:
- """Return of a tuple of the current time and the end and start times of the next October."""
- now = datetime.utcnow()
+ """Return of a tuple of the current time and the end and start times of the next Hacktober."""
+ now = datetime.now(tz=UTC)
year = now.year
if now.month > 10:
year += 1
- end = datetime(year, 11, 1, 12) # November 1st 12:00 (UTC-12:00)
- start = datetime(year, 9, 30, 10) # September 30th 10:00 (UTC+14:00)
+ end = datetime(year, 11, 1, 12, tzinfo=UTC) # November 1st 12:00 (UTC-12:00)
+ start = datetime(year, 9, 30, 10, tzinfo=UTC) # September 30th 10:00 (UTC+14:00)
return now, end, start
@commands.command()
diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py
index 8b012a17..15126f60 100644
--- a/bot/exts/events/trivianight/_game.py
+++ b/bot/exts/events/trivianight/_game.py
@@ -1,7 +1,8 @@
import time
+from collections.abc import Iterable
from random import randrange
from string import ascii_uppercase
-from typing import Iterable, NamedTuple, Optional, TypedDict
+from typing import NamedTuple, TypedDict
DEFAULT_QUESTION_POINTS = 10
DEFAULT_QUESTION_TIME = 20
@@ -14,8 +15,8 @@ class QuestionData(TypedDict):
description: str
answers: list[str]
correct: str
- points: Optional[int]
- time: Optional[int]
+ points: int | None
+ time: int | None
class UserGuess(NamedTuple):
@@ -26,15 +27,15 @@ class UserGuess(NamedTuple):
elapsed: float
-class QuestionClosed(RuntimeError):
+class QuestionClosedError(RuntimeError):
"""Exception raised when the question is not open for guesses anymore."""
-class AlreadyUpdated(RuntimeError):
+class AlreadyUpdatedError(RuntimeError):
"""Exception raised when the user has already updated their guess once."""
-class AllQuestionsVisited(RuntimeError):
+class AllQuestionsVisitedError(RuntimeError):
"""Exception raised when all of the questions have been visited."""
@@ -90,10 +91,10 @@ class Question:
def _update_guess(self, user: int, answer: str) -> UserGuess:
"""Update an already existing guess."""
if self._started is None:
- raise QuestionClosed("Question is not open for answers.")
+ raise QuestionClosedError("Question is not open for answers.")
if self._guesses[user][1] is False:
- raise AlreadyUpdated(f"User({user}) has already updated their guess once.")
+ raise AlreadyUpdatedError(f"User({user}) has already updated their guess once.")
self._guesses[user] = (answer, False, time.perf_counter() - self._started)
return self._guesses[user]
@@ -104,7 +105,7 @@ class Question:
return self._update_guess(user, answer)
if self._started is None:
- raise QuestionClosed("Question is not open for answers.")
+ raise QuestionClosedError("Question is not open for answers.")
self._guesses[user] = (answer, True, time.perf_counter() - self._started)
return self._guesses[user]
@@ -126,7 +127,7 @@ class TriviaNightGame:
self._questions = [Question(q) for q in data]
# A copy of the questions to keep for `.trivianight list`
self._all_questions = list(self._questions)
- self.current_question: Optional[Question] = None
+ self.current_question: Question | None = None
self._points = {}
self._speed = {}
@@ -148,7 +149,7 @@ class TriviaNightGame:
except IndexError:
raise ValueError(f"Question number {number} does not exist.")
elif len(self._questions) == 0:
- raise AllQuestionsVisited("All of the questions have been visited.")
+ raise AllQuestionsVisitedError("All of the questions have been visited.")
else:
question = self._questions.pop(randrange(len(self._questions)))
diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py
index 5f1046dc..a0dd545e 100644
--- a/bot/exts/events/trivianight/_questions.py
+++ b/bot/exts/events/trivianight/_questions.py
@@ -7,7 +7,7 @@ from discord.ui import Button, View
from bot.constants import Colours, NEGATIVE_REPLIES
-from ._game import AlreadyUpdated, Question, QuestionClosed
+from ._game import AlreadyUpdatedError, Question, QuestionClosedError
from ._scoreboard import Scoreboard
@@ -29,7 +29,7 @@ class AnswerButton(Button):
"""
try:
guess = self.question.guess(interaction.user.id, self.label)
- except AlreadyUpdated:
+ except AlreadyUpdatedError:
await interaction.response.send_message(
embed=Embed(
title=choice(NEGATIVE_REPLIES),
@@ -39,7 +39,7 @@ class AnswerButton(Button):
ephemeral=True
)
return
- except QuestionClosed:
+ except QuestionClosedError:
await interaction.response.send_message(
embed=Embed(
title=choice(NEGATIVE_REPLIES),
@@ -91,7 +91,7 @@ class QuestionView(View):
- text: A string that represents the question description to 'unicodeify'
"""
return "".join(
- f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter
+ f"{letter}\u200b" if letter not in ("\n", "\t", "`", "p", "y") else letter
for idx, letter in enumerate(text)
)
@@ -127,13 +127,13 @@ class QuestionView(View):
if len(guesses) != 0:
answers_chosen = {
answer_choice: len(
- tuple(filter(lambda x: x[0] == answer_choice, guesses.values())) # noqa: B023
+ tuple(filter(lambda x: x[0] == answer_choice, guesses.values()))
)
for answer_choice in labels
}
answers_chosen = dict(
- sorted(list(answers_chosen.items()), key=lambda item: item[1], reverse=True)
+ sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)
)
for answer, people_answered in answers_chosen.items():
diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py
index 10435551..e0db45d8 100644
--- a/bot/exts/events/trivianight/trivianight.py
+++ b/bot/exts/events/trivianight/trivianight.py
@@ -1,7 +1,6 @@
import asyncio
from json import JSONDecodeError, loads
from random import choice
-from typing import Optional
from discord import Embed
from discord.ext import commands
@@ -10,7 +9,7 @@ from bot.bot import Bot
from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles
from bot.utils.pagination import LinePaginator
-from ._game import AllQuestionsVisited, TriviaNightGame
+from ._game import AllQuestionsVisitedError, TriviaNightGame
from ._questions import QuestionView
from ._scoreboard import Scoreboard
@@ -23,8 +22,8 @@ class TriviaNightCog(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.game: Optional[TriviaNightGame] = None
- self.scoreboard: Optional[Scoreboard] = None
+ self.game: TriviaNightGame | None = None
+ self.scoreboard: Scoreboard | None = None
self.question_closed: asyncio.Event = None
@commands.group(aliases=["tn"], invoke_without_command=True)
@@ -46,7 +45,7 @@ class TriviaNightCog(commands.Cog):
@trivianight.command()
@commands.has_any_role(*TRIVIA_NIGHT_ROLES)
- async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None:
+ async def load(self, ctx: commands.Context, *, to_load: str | None) -> None:
"""
Loads a JSON file from the provided attachment or argument.
@@ -76,8 +75,7 @@ class TriviaNightCog(commands.Cog):
elif not to_load:
raise commands.BadArgument("You didn't attach an attachment nor link a message!")
elif (
- to_load.startswith("https://discord.com/channels")
- or to_load.startswith("https://discordapp.com/channels")
+ to_load.startswith(("https://discord.com/channels", "https://discordapp.com/channels"))
):
channel_id, message_id = to_load.split("/")[-2:]
channel = await ctx.guild.fetch_channel(int(channel_id))
@@ -92,7 +90,7 @@ class TriviaNightCog(commands.Cog):
try:
serialized_json = loads(json_text)
except JSONDecodeError as error:
- raise commands.BadArgument(f"Looks like something went wrong:\n{str(error)}")
+ raise commands.BadArgument(f"Looks like something went wrong:\n{error!s}")
self.game = TriviaNightGame(serialized_json)
self.question_closed = asyncio.Event()
@@ -107,7 +105,7 @@ class TriviaNightCog(commands.Cog):
await ctx.send(embed=success_embed)
- @trivianight.command(aliases=('next',))
+ @trivianight.command(aliases=("next",))
@commands.has_any_role(*TRIVIA_NIGHT_ROLES)
async def question(self, ctx: commands.Context, question_number: str = None) -> None:
"""
@@ -135,7 +133,7 @@ class TriviaNightCog(commands.Cog):
try:
next_question = self.game.next_question(question_number)
- except AllQuestionsVisited:
+ except AllQuestionsVisitedError:
error_embed = Embed(
title=choice(NEGATIVE_REPLIES),
description="All of the questions have been used.",
diff --git a/bot/exts/fun/anagram.py b/bot/exts/fun/anagram.py
index d8ea6a55..8210d1d5 100644
--- a/bot/exts/fun/anagram.py
+++ b/bot/exts/fun/anagram.py
@@ -15,7 +15,7 @@ log = logging.getLogger(__name__)
TIME_LIMIT = 60
# anagram.json file contains all the anagrams
-with open(Path("bot/resources/fun/anagram.json"), "r") as f:
+with open(Path("bot/resources/fun/anagram.json")) as f:
ANAGRAMS_ALL = json.load(f)
diff --git a/bot/exts/fun/battleship.py b/bot/exts/fun/battleship.py
index a8039cf2..4a552605 100644
--- a/bot/exts/fun/battleship.py
+++ b/bot/exts/fun/battleship.py
@@ -4,7 +4,6 @@ import random
import re
from dataclasses import dataclass
from functools import partial
-from typing import Optional
import discord
from discord.ext import commands
@@ -19,7 +18,7 @@ log = logging.getLogger(__name__)
class Square:
"""Each square on the battleship grid - if they contain a boat and if they've been aimed at."""
- boat: Optional[str]
+ boat: str | None
aimed: bool
@@ -31,8 +30,8 @@ EmojiSet = dict[tuple[bool, bool], str]
class Player:
"""Each player in the game - their messages for the boards and their current grid."""
- user: Optional[discord.Member]
- board: Optional[discord.Message]
+ user: discord.Member | None
+ board: discord.Message | None
opponent_board: discord.Message
grid: Grid
@@ -110,10 +109,10 @@ class Game:
self.gameover: bool = False
- self.turn: Optional[Player] = None
- self.next: Optional[Player] = None
+ self.turn: Player | None = None
+ self.next: Player | None = None
- self.match: Optional[re.Match] = None
+ self.match: re.Match | None = None
self.surrender: bool = False
self.setup_grids()
@@ -135,7 +134,7 @@ class Game:
for row in player.grid
]
- rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid)]
+ rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid, strict=True)]
return "\n".join([LETTERS] + rows)
@staticmethod
@@ -215,7 +214,7 @@ class Game:
(self.p1, "board"), (self.p2, "board")
)
- for board, location in zip(boards, locations):
+ for board, location in zip(boards, locations, strict=True):
player, attr = location
if getattr(player, attr):
await getattr(player, attr).edit(content=board)
@@ -232,8 +231,9 @@ class Game:
if not self.match:
self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI))
return bool(self.match)
+ return None
- async def take_turn(self) -> Optional[Square]:
+ async def take_turn(self) -> Square | None:
"""Lets the player who's turn it is choose a square."""
square = None
turn_message = await self.turn.user.send(
diff --git a/bot/exts/fun/catify.py b/bot/exts/fun/catify.py
index 6e8c75ba..c1677cd8 100644
--- a/bot/exts/fun/catify.py
+++ b/bot/exts/fun/catify.py
@@ -1,21 +1,22 @@
import random
from contextlib import suppress
-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.constants import Colours, NEGATIVE_REPLIES
from bot.utils import helpers
+CATS = ["ᓚᘏᗢ", "ᘡᘏᗢ", "🐈", "ᓕᘏᗢ", "ᓇᘏᗢ", "ᓂᘏᗢ", "ᘣᘏᗢ", "ᕦᘏᗢ", "ᕂᘏᗢ"]
+
class Catify(commands.Cog):
"""Cog for the catify command."""
@commands.command(aliases=("ᓚᘏᗢify", "ᓚᘏᗢ"))
@commands.cooldown(1, 5, commands.BucketType.user)
- async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None:
+ async def catify(self, ctx: commands.Context, *, text: str | None) -> None:
"""
Convert the provided text into a cat themed sentence by interspercing cats throughout text.
@@ -36,13 +37,12 @@ class Catify(commands.Cog):
await ctx.send(embed=embed)
return
- else:
- display_name += f" | {random.choice(Cats.cats)}"
+ display_name += f" | {random.choice(CATS)}"
- await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none())
+ await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none())
- with suppress(Forbidden):
- await ctx.author.edit(nick=display_name)
+ with suppress(Forbidden):
+ await ctx.author.edit(nick=display_name)
else:
if len(text) >= 1500:
embed = Embed(
@@ -58,21 +58,21 @@ class Catify(commands.Cog):
name = name.lower()
if "cat" in name:
if random.randint(0, 5) == 5:
- string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**")
+ string_list[index] = name.replace("cat", f"**{random.choice(CATS)}**")
else:
- string_list[index] = name.replace("cat", random.choice(Cats.cats))
- for element in Cats.cats:
- if element in name:
- string_list[index] = name.replace(element, "cat")
+ string_list[index] = name.replace("cat", random.choice(CATS))
+ for cat in CATS:
+ if cat in name:
+ string_list[index] = name.replace(cat, "cat")
string_len = len(string_list) // 3 or len(string_list)
for _ in range(random.randint(1, string_len)):
# insert cat at random index
if random.randint(0, 5) == 5:
- string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**")
+ string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(CATS)}**")
else:
- string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats))
+ string_list.insert(random.randint(0, len(string_list)), random.choice(CATS))
text = helpers.suppress_links(" ".join(string_list))
await ctx.send(
diff --git a/bot/exts/fun/connect_four.py b/bot/exts/fun/connect_four.py
index 0d870a6e..6544dc48 100644
--- a/bot/exts/fun/connect_four.py
+++ b/bot/exts/fun/connect_four.py
@@ -1,7 +1,6 @@
import asyncio
import random
from functools import partial
-from typing import Optional, Union
import discord
import emojis
@@ -14,8 +13,8 @@ from bot.constants import Emojis
NUMBERS = list(Emojis.number_emojis.values())
CROSS_EMOJI = Emojis.incident_unactioned
-Coordinate = Optional[tuple[int, int]]
-EMOJI_CHECK = Union[discord.Emoji, str]
+Coordinate = tuple[int, int] | None
+EMOJI_CHECK = discord.Emoji | str
class Game:
@@ -26,7 +25,7 @@ class Game:
bot: Bot,
channel: discord.TextChannel,
player1: discord.Member,
- player2: Optional[discord.Member],
+ player2: discord.Member | None,
tokens: list[str],
size: int = 7
):
@@ -73,7 +72,7 @@ class Game:
await self.message.edit(content=None, embed=embed)
async def game_over(
- self, action: str, player1: Union[ClientUser, Member], player2: Union[ClientUser, Member]
+ self, action: str, player1: ClientUser | Member, player2: ClientUser | Member
) -> None:
"""Announces to public chat."""
if action == "win":
@@ -134,12 +133,12 @@ class Game:
reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0)
except asyncio.TimeoutError:
await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!")
- return
+ return None
else:
await message.delete()
if str(reaction.emoji) == CROSS_EMOJI:
await self.game_over("quit", self.player_active, self.player_inactive)
- return
+ return None
await self.message.remove_reaction(reaction, user)
@@ -197,7 +196,7 @@ class AI:
break
return possible_coords
- def check_ai_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]:
+ def check_ai_win(self, coord_list: list[Coordinate]) -> Coordinate:
"""
Check AI win.
@@ -205,12 +204,13 @@ class AI:
with 10% chance of not winning and returning None
"""
if random.randint(1, 10) == 1:
- return
+ return None
for coords in coord_list:
if self.game.check_win(coords, 2):
return coords
+ return None
- def check_player_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]:
+ def check_player_win(self, coord_list: list[Coordinate]) -> Coordinate | None:
"""
Check Player win.
@@ -218,17 +218,18 @@ class AI:
from winning with 25% of not blocking them and returning None.
"""
if random.randint(1, 4) == 1:
- return
+ return None
for coords in coord_list:
if self.game.check_win(coords, 1):
return coords
+ return None
@staticmethod
def random_coords(coord_list: list[Coordinate]) -> Coordinate:
"""Picks a random coordinate from the possible ones."""
return random.choice(coord_list)
- def play(self) -> Union[Coordinate, bool]:
+ def play(self) -> Coordinate | bool:
"""
Plays for the AI.
@@ -331,7 +332,7 @@ class ConnectFour(commands.Cog):
@staticmethod
def check_emojis(
e1: EMOJI_CHECK, e2: EMOJI_CHECK
- ) -> tuple[bool, Optional[str]]:
+ ) -> tuple[bool, str | None]:
"""Validate the emojis, the user put."""
if isinstance(e1, str) and emojis.count(e1) != 1:
return False, e1
@@ -342,7 +343,7 @@ class ConnectFour(commands.Cog):
async def _play_game(
self,
ctx: commands.Context,
- user: Optional[discord.Member],
+ user: discord.Member | None,
board_size: int,
emoji1: str,
emoji2: str
diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py
index a2612e51..fbdc9ea2 100644
--- a/bot/exts/fun/duck_game.py
+++ b/bot/exts/fun/duck_game.py
@@ -42,7 +42,7 @@ CARD_HEIGHT = 97
EMOJI_WRONG = "\u274C"
-ANSWER_REGEX = re.compile(r'^\D*(\d+)\D+(\d+)\D+(\d+)\D*$')
+ANSWER_REGEX = re.compile(r"^\D*(\d+)\D+(\d+)\D+(\d+)\D*$")
HELP_TEXT = """
**Each card has 4 features**
@@ -97,7 +97,7 @@ def get_card_image(card: tuple[int]) -> Image:
def as_trinary(card: tuple[int]) -> int:
"""Find the card's unique index by interpreting its features as trinary."""
- return int(''.join(str(x) for x in card), base=3)
+ return int("".join(str(x) for x in card), base=3)
class DuckGame:
@@ -156,7 +156,7 @@ class DuckGame:
# which is prevented by the triangle iteration.
completion = tuple(
feat_a if feat_a == feat_b else 3-feat_a-feat_b
- for feat_a, feat_b in zip(card_a, card_b)
+ for feat_a, feat_b in zip(card_a, card_b, strict=True)
)
try:
idx_c = self.board.index(completion)
@@ -178,8 +178,8 @@ class DuckGamesDirector(commands.Cog):
self.current_games = {}
@commands.group(
- name='duckduckduckgoose',
- aliases=['dddg', 'ddg', 'duckduckgoose', 'duckgoose'],
+ name="duckduckduckgoose",
+ aliases=["dddg", "ddg", "duckduckgoose", "duckgoose"],
invoke_without_command=True
)
@commands.cooldown(rate=1, per=2, type=commands.BucketType.channel)
@@ -218,7 +218,7 @@ class DuckGamesDirector(commands.Cog):
return
game = self.current_games[channel.id]
- if msg.content.strip().lower() == 'goose':
+ if msg.content.strip().lower() == "goose":
# If all of the solutions have been claimed, i.e. the "goose" call is correct.
if len(game.solutions) == len(game.claimed_answers):
try:
@@ -248,7 +248,7 @@ class DuckGamesDirector(commands.Cog):
if answer in game.solutions:
game.claimed_answers[answer] = msg.author
game.scores[msg.author] += CORRECT_SOLN
- await self.append_to_found_embed(game, f"{str(answer):12s} - {msg.author.display_name}")
+ await self.append_to_found_embed(game, f"{answer!s:12s} - {msg.author.display_name}")
else:
await msg.add_reaction(EMOJI_WRONG)
game.scores[msg.author] += INCORRECT_SOLN
diff --git a/bot/exts/fun/game.py b/bot/exts/fun/game.py
index a8b0b3a0..b2b18f04 100644
--- a/bot/exts/fun/game.py
+++ b/bot/exts/fun/game.py
@@ -2,9 +2,9 @@ import difflib
import logging
import random
import re
-from datetime import datetime as dt, timedelta
+from datetime import UTC, datetime, timedelta
from enum import IntEnum
-from typing import Any, Optional
+from typing import Any
from aiohttp import ClientSession
from discord import Embed
@@ -20,8 +20,8 @@ from bot.utils.pagination import ImagePaginator, LinePaginator
# Base URL of IGDB API
BASE_URL = "https://api.igdb.com/v4"
-CLIENT_ID = Tokens.igdb_client_id
-CLIENT_SECRET = Tokens.igdb_client_secret
+CLIENT_ID = Tokens.igdb_client_id.get_secret_value()
+CLIENT_SECRET = Tokens.igdb_client_secret.get_secret_value()
# The number of seconds before expiry that we attempt to re-fetch a new access token
ACCESS_TOKEN_RENEWAL_WINDOW = 60*60*24*2
@@ -255,7 +255,7 @@ class Games(Cog):
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:
+ async def games(self, ctx: Context, amount: int | None = 5, *, genre: str | None) -> None:
"""
Get random game(s) by genre from IGDB. Use .games genres command to get all available genres.
@@ -293,7 +293,8 @@ class Games(Cog):
f"{f'Maybe you meant `{display_possibilities}`?' if display_possibilities else ''}"
)
return
- elif len(possibilities) == 1:
+
+ if len(possibilities) == 1:
games = await self.get_games_list(
amount, self.genres[possibilities[0][1]], offset=random.randint(0, 150)
)
@@ -368,8 +369,8 @@ class Games(Cog):
async def get_games_list(
self,
amount: int,
- genre: Optional[str] = None,
- sort: Optional[str] = None,
+ genre: str | None = None,
+ sort: str | None = None,
additional_body: str = "",
offset: int = 0
) -> list[dict[str, Any]]:
@@ -398,10 +399,13 @@ class Games(Cog):
async def create_page(self, data: dict[str, Any]) -> tuple[str, str]:
"""Create content of Game Page."""
# Create cover image URL from template
- url = COVER_URL.format(**{"image_id": data["cover"]["image_id"] if "cover" in data else ""})
+ url = COVER_URL.format(image_id=data.get("cover", {}).get("image_id", ""))
# Get release date separately with checking
- release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?"
+ if "first_release_date" in data:
+ release_date = datetime.fromtimestamp(data["first_release_date"], tz=UTC).date()
+ else:
+ release_date = "?"
# Create Age Ratings value
rating = ", ".join(
@@ -434,7 +438,7 @@ class Games(Cog):
lines = []
# Define request body of IGDB API request and do request
- body = SEARCH_BODY.format(**{"term": search_term})
+ body = SEARCH_BODY.format(term=search_term)
async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp:
data = await resp.json()
@@ -460,10 +464,10 @@ class Games(Cog):
returning results.
"""
# Create request body from template
- body = COMPANIES_LIST_BODY.format(**{
- "limit": limit,
- "offset": offset
- })
+ body = COMPANIES_LIST_BODY.format(
+ limit=limit,
+ offset=offset,
+ )
async with self.http_session.post(url=f"{BASE_URL}/companies", data=body, headers=self.headers) as resp:
return await resp.json()
@@ -471,10 +475,10 @@ class Games(Cog):
async def create_company_page(self, data: dict[str, Any]) -> tuple[str, str]:
"""Create good formatted Game Company page."""
# Generate URL of company logo
- url = LOGO_URL.format(**{"image_id": data["logo"]["image_id"] if "logo" in data else ""})
+ url = LOGO_URL.format(image_id=data.get("logo", {}).get("image_id", ""))
# Try to get found date of company
- founded = dt.utcfromtimestamp(data["start_date"]).date() if "start_date" in data else "?"
+ founded = datetime.fromtimestamp(data["start_date"], tz=UTC).date() if "start_date" in data else "?"
# Generate list of games, that company have developed or published
developed = ", ".join(game["name"] for game in data["developed"]) if "developed" in data else "?"
diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py
index f385a955..7a02a552 100644
--- a/bot/exts/fun/hangman.py
+++ b/bot/exts/fun/hangman.py
@@ -89,7 +89,7 @@ class Hangman(commands.Cog):
word = choice(filtered_words)
# `pretty_word` is used for comparing the indices where the guess of the user is similar to the word
# The `user_guess` variable is prettified by adding spaces between every dash, and so is the `pretty_word`
- pretty_word = ''.join([f"{letter} " for letter in word])[:-1]
+ pretty_word = "".join([f"{letter} " for letter in word])[:-1]
user_guess = ("_ " * len(word))[:-1]
tries = 6
guessed_letters = set()
@@ -104,7 +104,7 @@ class Hangman(commands.Cog):
))
# Game loop
- while user_guess.replace(' ', '') != word:
+ while user_guess.replace(" ", "") != word:
# Edit the message to the current state of the game
await original_message.edit(embed=self.create_embed(tries, user_guess))
@@ -136,7 +136,7 @@ class Hangman(commands.Cog):
continue
# Checks for repeated guesses
- elif normalized_content in guessed_letters:
+ if normalized_content in guessed_letters:
already_guessed_embed = Embed(
title=choice(NEGATIVE_REPLIES),
description=f"You have already guessed `{normalized_content}`, try again!",
@@ -146,12 +146,11 @@ class Hangman(commands.Cog):
continue
# Checks for correct guesses from the user
- elif normalized_content in word:
+ if normalized_content in word:
positions = {idx for idx, letter in enumerate(pretty_word) if letter == normalized_content}
user_guess = "".join(
[normalized_content if index in positions else dash for index, dash in enumerate(user_guess)]
)
-
else:
tries -= 1
diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py
index 8af05413..12f2d0b6 100644
--- a/bot/exts/fun/latex.py
+++ b/bot/exts/fun/latex.py
@@ -1,19 +1,22 @@
import hashlib
+import logging
import os
import re
import string
from io import BytesIO
from pathlib import Path
-from typing import BinaryIO, Optional
+from typing import BinaryIO
import discord
from PIL import Image
+from aiohttp import web
from discord.ext import commands
from bot.bot import Bot
from bot.constants import Channels, WHITELISTED_CHANNELS
from bot.utils.decorators import whitelist_override
+log = logging.getLogger(__name__)
FORMATTED_CODE_REGEX = re.compile(
r"(?P<delim>(?P<block>```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block
r"(?(block)(?:(?P<lang>[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline)
@@ -45,8 +48,7 @@ def _prepare_input(text: str) -> str:
"""Extract latex from a codeblock, if it is in one."""
if match := FORMATTED_CODE_REGEX.match(text):
return match.group("code")
- else:
- return text
+ return text
def _process_image(data: bytes, out_file: BinaryIO) -> None:
@@ -65,7 +67,7 @@ def _process_image(data: bytes, out_file: BinaryIO) -> None:
class InvalidLatexError(Exception):
"""Represents an error caused by invalid latex."""
- def __init__(self, logs: Optional[str]):
+ def __init__(self, logs: str | None):
super().__init__(logs)
self.logs = logs
@@ -89,7 +91,7 @@ class Latex(commands.Cog):
) as response:
_process_image(await response.read(), out_file)
- async def _upload_to_pastebin(self, text: str) -> Optional[str]:
+ async def _upload_to_pastebin(self, text: str) -> str | None:
"""Uploads `text` to the paste service, returning the url if successful."""
try:
async with self.bot.http_session.post(
@@ -100,9 +102,8 @@ class Latex(commands.Cog):
response_json = await response.json()
if "key" in response_json:
return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect"
- except Exception:
- # 400 (Bad Request) means there are too many characters
- pass
+ except web.HTTPClientError as e:
+ log.info("Error when uploading latex output to pastebin. %s", e)
@commands.command()
@commands.max_concurrency(1, commands.BucketType.guild, wait=True)
@@ -112,7 +113,7 @@ class Latex(commands.Cog):
query = _prepare_input(query)
# the hash of the query is used as the filename in the cache.
- query_hash = hashlib.md5(query.encode()).hexdigest()
+ query_hash = hashlib.md5(query.encode()).hexdigest() # noqa: S324
image_path = CACHE_DIRECTORY / f"{query_hash}.png"
async with ctx.typing():
if not image_path.exists():
diff --git a/bot/exts/fun/madlibs.py b/bot/exts/fun/madlibs.py
index 075dde75..c14e8a3a 100644
--- a/bot/exts/fun/madlibs.py
+++ b/bot/exts/fun/madlibs.py
@@ -117,7 +117,7 @@ class Madlibs(commands.Cog):
self.checks.remove(author_check)
story = []
- for value, blank in zip(random_template["value"], blanks):
+ for value, blank in zip(random_template["value"], blanks, strict=True):
story.append(f"{value}__{blank}__")
# In each story template, there is always one more "value"
diff --git a/bot/exts/fun/minesweeper.py b/bot/exts/fun/minesweeper.py
index f16b1db2..69be88d3 100644
--- a/bot/exts/fun/minesweeper.py
+++ b/bot/exts/fun/minesweeper.py
@@ -2,7 +2,6 @@ import logging
from collections.abc import Iterator
from dataclasses import dataclass
from random import randint, random
-from typing import Union
import discord
from discord.ext import commands
@@ -33,7 +32,7 @@ MESSAGE_MAPPING = {
log = logging.getLogger(__name__)
-GameBoard = list[list[Union[str, int]]]
+GameBoard = list[list[str | int]]
@dataclass
@@ -205,9 +204,9 @@ class Minesweeper(commands.Cog):
for y in range(10)
):
return False
- else:
- await self.won(ctx)
- return True
+
+ await self.won(ctx)
+ return True
async def reveal_one(
self,
@@ -227,7 +226,7 @@ class Minesweeper(commands.Cog):
await self.lost(ctx)
revealed[y][x] = "x" # mark bomb that made you lose with a x
return True
- elif board[y][x] == 0:
+ if board[y][x] == 0:
self.reveal_zeros(revealed, board, x, y)
return await self.check_if_won(ctx, revealed, board)
diff --git a/bot/exts/fun/movie.py b/bot/exts/fun/movie.py
index 422a83ac..3d36b119 100644
--- a/bot/exts/fun/movie.py
+++ b/bot/exts/fun/movie.py
@@ -22,7 +22,7 @@ THUMBNAIL_URL = "https://i.imgur.com/LtFtC8H.png"
# Define movie params, that will be used for every movie request
MOVIE_PARAMS = {
- "api_key": Tokens.tmdb,
+ "api_key": Tokens.tmdb.get_secret_value(),
"language": "en-US"
}
@@ -63,17 +63,17 @@ class Movie(Cog):
@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.
+ Get random movies by specifying genre.
- Default 5. Use .movies genres to get all available genres.
+ The amount parameter, that defines how many movies will be shown, defaults to 5.
+ Use `.movies genres` to get all available genres.
"""
# Check is there more than 20 movies specified, due TMDB return 20 movies
# per page, so this is max. Also you can't get less movies than 1, just logic
if amount > 20:
await ctx.send("You can't get more than 20 movies at once. (TMDB limits)")
return
- elif amount < 1:
+ if amount < 1:
await ctx.send("You can't get less than 1 movie.")
return
@@ -106,7 +106,7 @@ class Movie(Cog):
"""Return JSON of TMDB discover request."""
# Define params of request
params = {
- "api_key": Tokens.tmdb,
+ "api_key": Tokens.tmdb.get_secret_value(),
"language": "en-US",
"sort_by": "popularity.desc",
"include_adult": "false",
@@ -179,8 +179,8 @@ class Movie(Cog):
text += "__**Some Numbers**__\n"
- budget = f"{movie['budget']:,d}" if movie['budget'] else "?"
- revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?"
+ 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)
@@ -208,4 +208,7 @@ class Movie(Cog):
async def setup(bot: Bot) -> None:
"""Load the Movie Cog."""
+ if not Tokens.tmdb:
+ logger.warning("No TMDB token. Not loading Movie Cog.")
+ return
await bot.add_cog(Movie(bot))
diff --git a/bot/exts/fun/quack.py b/bot/exts/fun/quack.py
index bb0cd731..9bb024fc 100644
--- a/bot/exts/fun/quack.py
+++ b/bot/exts/fun/quack.py
@@ -1,6 +1,6 @@
import logging
import random
-from typing import Literal, Optional
+from typing import Literal
import discord
from discord.ext import commands
@@ -8,7 +8,7 @@ from discord.ext import commands
from bot.bot import Bot
from bot.constants import Colours, NEGATIVE_REPLIES
-API_URL = 'https://quackstack.pythondiscord.com'
+API_URL = "https://quackstack.pythondiscord.com"
log = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ class Quackstack(commands.Cog):
ctx: commands.Context,
ducktype: Literal["duck", "manduck"] = "duck",
*,
- seed: Optional[str] = None
+ seed: str | None = None
) -> None:
"""
Use the Quackstack API to generate a random duck.
diff --git a/bot/exts/fun/snakes/__init__.py b/bot/exts/fun/snakes/__init__.py
index 8aa39fb5..be71ac44 100644
--- a/bot/exts/fun/snakes/__init__.py
+++ b/bot/exts/fun/snakes/__init__.py
@@ -1,6 +1,7 @@
import logging
from bot.bot import Bot
+from bot.constants import Tokens
from bot.exts.fun.snakes._snakes_cog import Snakes
log = logging.getLogger(__name__)
@@ -8,4 +9,6 @@ log = logging.getLogger(__name__)
async def setup(bot: Bot) -> None:
"""Load the Snakes Cog."""
+ if not Tokens.youtube:
+ log.warning("No Youtube token. All YouTube related commands in Snakes cog won't work.")
await bot.add_cog(Snakes(bot))
diff --git a/bot/exts/fun/snakes/_snakes_cog.py b/bot/exts/fun/snakes/_snakes_cog.py
index d0542c23..eca462c6 100644
--- a/bot/exts/fun/snakes/_snakes_cog.py
+++ b/bot/exts/fun/snakes/_snakes_cog.py
@@ -9,7 +9,7 @@ import textwrap
import urllib
from functools import partial
from io import BytesIO
-from typing import Any, Optional
+from typing import Any
import async_timeout
from PIL import Image, ImageDraw, ImageFont
@@ -274,7 +274,7 @@ class Snakes(Cog):
return message
- async def _fetch(self, url: str, params: Optional[dict] = None) -> dict:
+ async def _fetch(self, url: str, params: dict | None = None) -> dict:
"""Asynchronous web request helper method."""
if params is None:
params = {}
@@ -518,52 +518,51 @@ class Snakes(Cog):
log.debug("Antidote timed out waiting for a reaction")
break # We're done, no reactions for the last 5 minutes
- if antidote_tries < 10:
- if antidote_guess_count < 4:
- if reaction.emoji in ANTIDOTE_EMOJI:
- antidote_guess_list.append(reaction.emoji)
- antidote_guess_count += 1
-
- if antidote_guess_count == 4: # Guesses complete
- antidote_guess_count = 0
- page_guess_list[antidote_tries] = " ".join(antidote_guess_list)
-
- # Now check guess
- for i in range(0, len(antidote_answer)):
- if antidote_guess_list[i] == antidote_answer[i]:
- guess_result.append(TICK_EMOJI)
- elif antidote_guess_list[i] in antidote_answer:
- guess_result.append(BLANK_EMOJI)
- else:
- guess_result.append(CROSS_EMOJI)
- guess_result.sort()
- page_result_list[antidote_tries] = " ".join(guess_result)
-
- # Rebuild the board
- board = []
- for i in range(0, 10):
- board.append(f"`{i+1:02d}` "
- f"{page_guess_list[i]} - "
- f"{page_result_list[i]}")
- board.append(EMPTY_UNICODE)
-
- # Remove Reactions
- for emoji in antidote_guess_list:
- await board_id.remove_reaction(emoji, user)
-
- if antidote_guess_list == antidote_answer:
- win = True
-
- antidote_tries += 1
- guess_result = []
- antidote_guess_list = []
-
- antidote_embed.clear_fields()
- antidote_embed.add_field(name=f"{10 - antidote_tries} "
- f"guesses remaining",
- value="\n".join(board))
- # Redisplay the board
- await board_id.edit(embed=antidote_embed)
+ if antidote_tries < 10 and antidote_guess_count < 4:
+ if reaction.emoji in ANTIDOTE_EMOJI:
+ antidote_guess_list.append(reaction.emoji)
+ antidote_guess_count += 1
+
+ if antidote_guess_count == 4: # Guesses complete
+ antidote_guess_count = 0
+ page_guess_list[antidote_tries] = " ".join(antidote_guess_list)
+
+ # Now check guess
+ for i in range(0, len(antidote_answer)):
+ if antidote_guess_list[i] == antidote_answer[i]:
+ guess_result.append(TICK_EMOJI)
+ elif antidote_guess_list[i] in antidote_answer:
+ guess_result.append(BLANK_EMOJI)
+ else:
+ guess_result.append(CROSS_EMOJI)
+ guess_result.sort()
+ page_result_list[antidote_tries] = " ".join(guess_result)
+
+ # Rebuild the board
+ board = []
+ for i in range(0, 10):
+ board.append(f"`{i+1:02d}` "
+ f"{page_guess_list[i]} - "
+ f"{page_result_list[i]}")
+ board.append(EMPTY_UNICODE)
+
+ # Remove Reactions
+ for emoji in antidote_guess_list:
+ await board_id.remove_reaction(emoji, user)
+
+ if antidote_guess_list == antidote_answer:
+ win = True
+
+ antidote_tries += 1
+ guess_result = []
+ antidote_guess_list = []
+
+ antidote_embed.clear_fields()
+ antidote_embed.add_field(name=f"{10 - antidote_tries} "
+ f"guesses remaining",
+ value="\n".join(board))
+ # Redisplay the board
+ await board_id.edit(embed=antidote_embed)
# Winning / Ending Screen
if win is True:
@@ -746,10 +745,10 @@ class Snakes(Cog):
await message.delete()
# Build and send the embed.
- my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name))
+ my_snake_embed = Embed(description=f":tada: Congrats! You hatched: **{snake_name}**")
my_snake_embed.set_thumbnail(url=snake_image)
my_snake_embed.set_footer(
- text=" Owner: {0}#{1}".format(ctx.author.name, ctx.author.discriminator)
+ text=f" Owner: {ctx.author.name}#{ctx.author.discriminator}"
)
await ctx.send(embed=my_snake_embed)
@@ -773,7 +772,7 @@ class Snakes(Cog):
"query": "snake",
"page": page,
"language": "en-US",
- "api_key": Tokens.tmdb,
+ "api_key": Tokens.tmdb.get_secret_value(),
}
)
data = await response.json()
@@ -785,7 +784,7 @@ class Snakes(Cog):
f"https://api.themoviedb.org/3/movie/{movie}",
params={
"language": "en-US",
- "api_key": Tokens.tmdb,
+ "api_key": Tokens.tmdb.get_secret_value(),
}
)
data = await response.json()
@@ -832,7 +831,7 @@ class Snakes(Cog):
# Prepare a question.
question = random.choice(self.snake_quizzes)
answer = question["answerkey"]
- options = {key: question["options"][key] for key in ANSWERS_EMOJI.keys()}
+ options = {key: question["options"][key] for key in ANSWERS_EMOJI}
# Build and send the embed.
embed = Embed(
@@ -879,10 +878,7 @@ class Snakes(Cog):
snake_name = snake_name.split()[-1]
# If no name is provided, use whoever called the command.
- if name:
- user_name = name
- else:
- user_name = ctx.author.display_name
+ user_name = name if name else ctx.author.display_name
# Get the index of the vowel to slice the username at
user_slice_index = len(user_name)
@@ -1095,7 +1091,7 @@ class Snakes(Cog):
"part": "snippet",
"q": urllib.parse.quote_plus(query),
"type": "video",
- "key": Tokens.youtube
+ "key": Tokens.youtube.get_secret_value()
}
)
response = await response.json()
@@ -1148,3 +1144,4 @@ class Snakes(Cog):
embed.description = "Could not generate the snake card! Please try again."
embed.title = random.choice(ERROR_REPLIES)
await ctx.send(embed=embed)
+ # endregion
diff --git a/bot/exts/fun/snakes/_utils.py b/bot/exts/fun/snakes/_utils.py
index 182fa9d9..ffffcd34 100644
--- a/bot/exts/fun/snakes/_utils.py
+++ b/bot/exts/fun/snakes/_utils.py
@@ -6,7 +6,6 @@ import math
import random
from itertools import product
from pathlib import Path
-from typing import Union
from PIL import Image
from PIL.ImageDraw import ImageDraw
@@ -132,7 +131,7 @@ def lerp(t: float, a: float, b: float) -> float:
return a + t * (b - a)
-class PerlinNoiseFactory(object):
+class PerlinNoiseFactory:
"""
Callable that produces Perlin noise for an arbitrary point in an arbitrary number of dimensions.
@@ -396,7 +395,7 @@ class SnakeAndLaddersGame:
Listen for reactions until players have joined, and the game has been started.
"""
- def startup_event_check(reaction_: Reaction, user_: Union[User, Member]) -> bool:
+ def startup_event_check(reaction_: Reaction, user_: User | Member) -> bool:
"""Make sure that this reaction is what we want to operate on."""
return (
all((
@@ -445,14 +444,12 @@ class SnakeAndLaddersGame:
# Allow game author or non-playing moderation staff to cancel a waiting game
await self.cancel_game()
return
- else:
- await self.player_leave(user)
- elif reaction.emoji == START_EMOJI:
- if self.ctx.author == user:
- self.started = True
- await self.start_game(user)
- await startup.delete()
- break
+ await self.player_leave(user)
+ elif reaction.emoji == START_EMOJI and self.ctx.author == user:
+ self.started = True
+ await self.start_game(user)
+ await startup.delete()
+ break
await startup.remove_reaction(reaction.emoji, user)
@@ -461,7 +458,7 @@ class SnakeAndLaddersGame:
await self.cancel_game()
return # We're done, no reactions for the last 5 minutes
- async def _add_player(self, user: Union[User, Member]) -> None:
+ async def _add_player(self, user: User | Member) -> None:
"""Add player to game."""
self.players.append(user)
self.player_tiles[user.id] = 1
@@ -470,7 +467,7 @@ class SnakeAndLaddersGame:
im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE))
self.avatar_images[user.id] = im
- async def player_join(self, user: Union[User, Member]) -> None:
+ async def player_join(self, user: User | Member) -> None:
"""
Handle players joining the game.
@@ -492,11 +489,11 @@ class SnakeAndLaddersGame:
await self.channel.send(
f"**Snakes and Ladders**: {user.mention} has joined the game.\n"
- f"There are now {str(len(self.players))} players in the game.",
+ f"There are now {len(self.players)!s} players in the game.",
delete_after=10
)
- async def player_leave(self, user: Union[User, Member]) -> bool:
+ async def player_leave(self, user: User | Member) -> bool:
"""
Handle players leaving the game.
@@ -531,17 +528,17 @@ class SnakeAndLaddersGame:
await self.channel.send("**Snakes and Ladders**: Game has been canceled.")
self._destruct()
- async def start_game(self, user: Union[User, Member]) -> None:
+ async def start_game(self, user: User | Member) -> None:
"""
Allow the game author to begin the game.
The game cannot be started if the game is in a waiting state.
"""
- if not user == self.author:
+ if user != self.author:
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 self.state != "waiting":
await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10)
return
@@ -552,7 +549,7 @@ class SnakeAndLaddersGame:
async def start_round(self) -> None:
"""Begin the round."""
- def game_event_check(reaction_: Reaction, user_: Union[User, Member]) -> bool:
+ def game_event_check(reaction_: Reaction, user_: User | Member) -> bool:
"""Make sure that this reaction is what we want to operate on."""
return (
all((
@@ -626,8 +623,7 @@ class SnakeAndLaddersGame:
# Only allow non-playing moderation staff to cancel a running game
await self.cancel_game()
return
- else:
- is_surrendered = await self.player_leave(user)
+ is_surrendered = await self.player_leave(user)
await self.positions.remove_reaction(reaction.emoji, user)
@@ -645,7 +641,7 @@ class SnakeAndLaddersGame:
if not is_surrendered:
await self._complete_round()
- async def player_roll(self, user: Union[User, Member]) -> None:
+ async def player_roll(self, user: User | Member) -> None:
"""Handle the player's roll."""
if user.id not in self.player_tiles:
await self.channel.send(user.mention + " You are not in the match.", delete_after=10)
@@ -692,7 +688,7 @@ class SnakeAndLaddersGame:
await self.channel.send("**Snakes and Ladders**: " + winner.mention + " has won the game! :tada:")
self._destruct()
- def _check_winner(self) -> Union[User, Member]:
+ def _check_winner(self) -> User | Member:
"""Return a winning member if we're in the post-round state and there's a winner."""
if self.state != "post_round":
return None
@@ -717,6 +713,6 @@ class SnakeAndLaddersGame:
return x_level, y_level
@staticmethod
- def _is_moderator(user: Union[User, Member]) -> bool:
+ def _is_moderator(user: User | Member) -> bool:
"""Return True if the user is a Moderator."""
- return any(role.id in MODERATION_ROLES for role in getattr(user, 'roles', []))
+ return any(role.id in MODERATION_ROLES for role in getattr(user, "roles", []))
diff --git a/bot/exts/fun/space.py b/bot/exts/fun/space.py
index 22a89050..3a666bfc 100644
--- a/bot/exts/fun/space.py
+++ b/bot/exts/fun/space.py
@@ -1,7 +1,7 @@
import logging
import random
-from datetime import date, datetime
-from typing import Any, Optional
+from datetime import UTC, date, datetime
+from typing import Any
from urllib.parse import urlencode
from discord import Embed
@@ -53,7 +53,7 @@ class Space(Cog):
await self.bot.invoke_help_command(ctx)
@space.command(name="apod")
- async def apod(self, ctx: Context, date: Optional[str]) -> None:
+ async def apod(self, ctx: Context, date: str | None) -> None:
"""
Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD.
@@ -63,13 +63,13 @@ class Space(Cog):
# Parse date to params, when provided. Show error message when invalid formatting
if date:
try:
- apod_date = datetime.strptime(date, "%Y-%m-%d").date()
+ apod_date = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC).date()
except ValueError:
await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.")
return
- now = datetime.now().date()
- if APOD_MIN_DATE > apod_date or now < apod_date:
+ now = datetime.now(tz=UTC).date()
+ if apod_date < APOD_MIN_DATE or now < apod_date:
await ctx.send(f"Date must be between {APOD_MIN_DATE.isoformat()} and {now.isoformat()} (today).")
return
@@ -86,7 +86,7 @@ class Space(Cog):
)
@space.command(name="nasa")
- async def nasa(self, ctx: Context, *, search_term: Optional[str]) -> None:
+ async def nasa(self, ctx: Context, *, search_term: str | None) -> None:
"""Get random NASA information/facts + image. Support `search_term` parameter for more specific search."""
params = {
"media_type": "image"
@@ -111,11 +111,11 @@ class Space(Cog):
)
@space.command(name="epic")
- async def epic(self, ctx: Context, date: Optional[str]) -> None:
+ async def epic(self, ctx: Context, date: str | None) -> 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()
+ show_date = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC).date().isoformat()
except ValueError:
await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.")
return
@@ -147,7 +147,7 @@ class Space(Cog):
async def mars(
self,
ctx: Context,
- date: Optional[DateConverter],
+ date: DateConverter | None,
rover: str = "curiosity"
) -> None:
"""
@@ -158,10 +158,8 @@ class Space(Cog):
rover = rover.lower()
if rover not in self.rovers:
await ctx.send(
- (
- f"Invalid rover `{rover}`.\n"
- f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`"
- )
+ f"Invalid rover `{rover}`.\n"
+ f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`"
)
return
@@ -203,14 +201,14 @@ class Space(Cog):
async def fetch_from_nasa(
self,
endpoint: str,
- additional_params: Optional[dict[str, Any]] = None,
- base: Optional[str] = NASA_BASE_URL,
+ additional_params: dict[str, Any] | None = None,
+ base: str | None = NASA_BASE_URL,
use_api_key: bool = True
) -> dict[str, Any]:
"""Fetch information from NASA API, return result."""
params = {}
if use_api_key:
- params["api_key"] = Tokens.nasa
+ params["api_key"] = Tokens.nasa.get_secret_value()
# Add additional parameters to request parameters only when they provided by user
if additional_params is not None:
@@ -219,7 +217,7 @@ class Space(Cog):
async with self.http_session.get(url=f"{base}/{endpoint}?{urlencode(params)}") as resp:
return await resp.json()
- def create_nasa_embed(self, title: str, description: str, image: str, footer: Optional[str] = "") -> Embed:
+ def create_nasa_embed(self, title: str, description: str, image: str, footer: str | None = "") -> Embed:
"""Generate NASA commands embeds. Required: title, description and image URL, footer (addition) is optional."""
return Embed(
title=title,
diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py
index fa2a7531..d4ae7107 100644
--- a/bot/exts/fun/tic_tac_toe.py
+++ b/bot/exts/fun/tic_tac_toe.py
@@ -1,6 +1,6 @@
import asyncio
import random
-from typing import Callable, Optional, Union
+from collections.abc import Callable
import discord
from discord.ext.commands import Cog, Context, check, group
@@ -42,7 +42,7 @@ class Player:
self.ctx = ctx
self.symbol = symbol
- async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, Optional[int]]:
+ async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, int | None]:
"""
Get move from user.
@@ -106,7 +106,7 @@ class AI:
class Game:
"""Class that contains information and functions about Tic Tac Toe game."""
- def __init__(self, players: list[Union[Player, AI]], ctx: Context):
+ def __init__(self, players: list[Player | AI], ctx: Context):
self.players = players
self.ctx = ctx
self.channel = ctx.channel
@@ -125,13 +125,13 @@ class Game:
self.current = self.players[0]
self.next = self.players[1]
- self.winner: Optional[Union[Player, AI]] = None
- self.loser: Optional[Union[Player, AI]] = None
+ self.winner: Player | AI | None = None
+ self.loser: Player | AI | None = None
self.over = False
self.canceled = False
self.draw = False
- async def get_confirmation(self) -> tuple[bool, Optional[str]]:
+ async def get_confirmation(self) -> tuple[bool, str | None]:
"""
Ask does user want to play TicTacToe against requester. First player is always requester.
@@ -171,10 +171,10 @@ class Game:
await confirm_message.delete()
if reaction.emoji == Emojis.confirmation:
return True, None
- else:
- self.over = True
- self.canceled = True
- return False, "User declined"
+
+ self.over = True
+ self.canceled = True
+ return False, "User declined"
@staticmethod
async def add_reactions(msg: discord.Message) -> None:
@@ -186,7 +186,8 @@ class Game:
"""Get formatted tic-tac-toe board for message."""
board = list(self.board.values())
return "\n".join(
- (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3))
+ f"{board[line]} {board[line + 1]} {board[line + 2]}"
+ for line in range(0, len(board), 3)
)
async def play(self) -> None:
@@ -256,7 +257,7 @@ class TicTacToe(Cog):
@is_channel_free()
@is_requester_free()
@group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True)
- async def tic_tac_toe(self, ctx: Context, opponent: Optional[discord.User]) -> None:
+ async def tic_tac_toe(self, ctx: Context, opponent: discord.User | None) -> None:
"""Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field."""
if opponent == ctx.author:
await ctx.send("You can't play against yourself.")
diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py
index 31652374..28cd4657 100644
--- a/bot/exts/fun/trivia_quiz.py
+++ b/bot/exts/fun/trivia_quiz.py
@@ -6,10 +6,10 @@ import random
import re
import string
from collections import defaultdict
+from collections.abc import Callable
from dataclasses import dataclass
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
from pathlib import Path
-from typing import Callable, Optional
import discord
from discord.ext import commands, tasks
@@ -249,7 +249,7 @@ class TriviaQuiz(commands.Cog):
wiki_questions = []
# trivia_quiz.json follows a pattern, every new category starts with the next century.
start_id = 501
- yesterday = datetime.strftime(datetime.now() - timedelta(1), '%Y/%m/%d')
+ yesterday = datetime.strftime(datetime.now(tz=UTC) - timedelta(1), "%Y/%m/%d")
while error_fetches < MAX_ERROR_FETCH_TRIES:
async with self.bot.http_session.get(url=WIKI_FEED_API_URL.format(date=yesterday)) as r:
@@ -267,7 +267,7 @@ class TriviaQuiz(commands.Cog):
# Normalize the wikipedia article title to remove all punctuations from it
for word in re.split(r"[\s-]", title := article["normalizedtitle"]):
cleaned_title = re.sub(
- rf'\b{word.strip(string.punctuation)}\b', word, title, flags=re.IGNORECASE
+ rf"\b{word.strip(string.punctuation)}\b", word, title, flags=re.IGNORECASE
)
# Since the extract contains the article name sometimes this would replace all the matching words
@@ -279,7 +279,7 @@ class TriviaQuiz(commands.Cog):
for word in re.split(r"[\s-]", cleaned_title):
word = word.strip(string.punctuation)
secret_word = r"\*" * len(word)
- question = re.sub(rf'\b{word}\b', f"**{secret_word}**", question, flags=re.IGNORECASE)
+ question = re.sub(rf"\b{word}\b", f"**{secret_word}**", question, flags=re.IGNORECASE)
formatted_article_question = {
"id": start_id,
@@ -307,7 +307,7 @@ class TriviaQuiz(commands.Cog):
return json.loads(p.read_text(encoding="utf-8"))
@commands.group(name="quiz", aliases=("trivia", "triviaquiz"), invoke_without_command=True)
- async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None:
+ async def quiz_game(self, ctx: commands.Context, category: str | None, questions: int | None) -> None:
"""
Start a quiz!
@@ -550,7 +550,7 @@ class TriviaQuiz(commands.Cog):
if self.game_status[ctx.channel.id]:
# Check if the author is the game starter or a moderator.
if ctx.author == self.game_owners[ctx.channel.id] or any(
- role.id in MODERATION_ROLES for role in getattr(ctx.author, 'roles', [])
+ role.id in MODERATION_ROLES for role in getattr(ctx.author, "roles", [])
):
self.game_status[ctx.channel.id] = False
del self.game_owners[ctx.channel.id]
diff --git a/bot/exts/fun/uwu.py b/bot/exts/fun/uwu.py
index 7a9d55d0..488c68f3 100644
--- a/bot/exts/fun/uwu.py
+++ b/bot/exts/fun/uwu.py
@@ -30,7 +30,7 @@ EMOJIS = [
"o.O",
"-.-",
">w<",
- "σωσ",
+ "σωσ", # noqa: RUF001
"òωó",
"ʘwʘ",
":3",
@@ -74,7 +74,7 @@ class Emoji:
return bot.get_emoji(self.uid) is not None
@classmethod
- def from_match(cls, match: tuple[str, str, str]) -> t.Optional['Emoji']:
+ def from_match(cls, match: tuple[str, str, str]) -> t.Optional["Emoji"]:
"""Creates an Emoji from a regex match tuple."""
if not match or len(match) != 3 or not match[2].isdecimal():
return None
@@ -155,7 +155,7 @@ class Uwu(Cog):
return input_string
@commands.command(name="uwu", aliases=("uwuwize", "uwuify",))
- async def uwu_command(self, ctx: Context, *, text: t.Optional[str] = None) -> None:
+ async def uwu_command(self, ctx: Context, *, text: str | None = None) -> None:
"""
Echo an uwuified version the passed text.
diff --git a/bot/exts/fun/wonder_twins.py b/bot/exts/fun/wonder_twins.py
index 0c5b0a76..9385780d 100644
--- a/bot/exts/fun/wonder_twins.py
+++ b/bot/exts/fun/wonder_twins.py
@@ -11,8 +11,8 @@ class WonderTwins(Cog):
"""Cog for a Wonder Twins inspired command."""
def __init__(self):
- with open(Path.cwd() / "bot" / "resources" / "fun" / "wonder_twins.yaml", "r", encoding="utf-8") as f:
- info = yaml.load(f, Loader=yaml.FullLoader)
+ with open(Path.cwd() / "bot" / "resources" / "fun" / "wonder_twins.yaml", encoding="utf-8") as f:
+ info = yaml.safe_load(f)
self.water_types = info["water_types"]
self.objects = info["objects"]
self.adjectives = info["adjectives"]
diff --git a/bot/exts/fun/xkcd.py b/bot/exts/fun/xkcd.py
index 380c3c80..7b34795c 100644
--- a/bot/exts/fun/xkcd.py
+++ b/bot/exts/fun/xkcd.py
@@ -1,7 +1,6 @@
import logging
import re
from random import randint
-from typing import Optional, Union
from discord import Embed
from discord.ext import tasks
@@ -21,7 +20,7 @@ class XKCD(Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.latest_comic_info: dict[str, Union[str, int]] = {}
+ self.latest_comic_info: dict[str, str | int] = {}
self.get_latest_comic_info.start()
def cog_unload(self) -> None:
@@ -38,7 +37,7 @@ class XKCD(Cog):
log.debug(f"Failed to get latest XKCD comic information. Status code {resp.status}")
@command(name="xkcd")
- async def fetch_xkcd_comics(self, ctx: Context, comic: Optional[str]) -> None:
+ async def fetch_xkcd_comics(self, ctx: Context, comic: str | None) -> None:
"""
Getting an xkcd comic's information along with the image.
diff --git a/bot/exts/holidays/easter/bunny_name_generator.py b/bot/exts/holidays/easter/bunny_name_generator.py
index 50872ebc..3034da4a 100644
--- a/bot/exts/holidays/easter/bunny_name_generator.py
+++ b/bot/exts/holidays/easter/bunny_name_generator.py
@@ -3,7 +3,6 @@ import logging
import random
import re
from pathlib import Path
-from typing import Optional
from discord.ext import commands
@@ -18,7 +17,7 @@ class BunnyNameGenerator(commands.Cog):
"""Generate a random bunny name, or bunnify your Discord username!"""
@staticmethod
- def find_separators(displayname: str) -> Optional[list[str]]:
+ def find_separators(displayname: str) -> 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)
if displayname not in new_name:
@@ -26,7 +25,7 @@ class BunnyNameGenerator(commands.Cog):
return None
@staticmethod
- def find_vowels(displayname: str) -> Optional[str]:
+ def find_vowels(displayname: str) -> str | None:
"""
Finds vowels in the user's display name.
@@ -46,6 +45,7 @@ class BunnyNameGenerator(commands.Cog):
new_name = re.sub(exp, vowel_sub, displayname)
if new_name != displayname:
return new_name
+ return None
@staticmethod
def append_name(displayname: str) -> str:
@@ -77,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)
diff --git a/bot/exts/holidays/easter/earth_photos.py b/bot/exts/holidays/easter/earth_photos.py
index e60e2626..66f1b07b 100644
--- a/bot/exts/holidays/easter/earth_photos.py
+++ b/bot/exts/holidays/easter/earth_photos.py
@@ -12,7 +12,7 @@ API_URL = "https://api.unsplash.com/photos/random"
class EarthPhotos(commands.Cog):
- """This cog contains the command for earth photos."""
+ """The earth photos cog."""
def __init__(self, bot: Bot):
self.bot = bot
@@ -23,7 +23,7 @@ class EarthPhotos(commands.Cog):
async with ctx.typing():
async with self.bot.http_session.get(
API_URL,
- params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key}
+ params={"query": "planet_earth", "client_id": Tokens.unsplash.get_secret_value()}
) as r:
jsondata = await r.json()
linksdata = jsondata.get("urls")
@@ -37,7 +37,7 @@ class EarthPhotos(commands.Cog):
rf = "?utm_source=Sir%20Lancebot&utm_medium=referral"
async with self.bot.http_session.get(
downloadlinksdata.get("download_location"),
- params={"client_id": Tokens.unsplash_access_key}
+ params={"client_id": Tokens.unsplash.get_secret_value()}
) as _:
pass
@@ -59,7 +59,7 @@ class EarthPhotos(commands.Cog):
async def setup(bot: Bot) -> None:
"""Load the Earth Photos cog."""
- if not Tokens.unsplash_access_key:
+ if not Tokens.unsplash:
log.warning("No Unsplash access key found. Cog not loading.")
return
await bot.add_cog(EarthPhotos(bot))
diff --git a/bot/exts/holidays/easter/easter_riddle.py b/bot/exts/holidays/easter/easter_riddle.py
index c5d7b164..6c29dd17 100644
--- a/bot/exts/holidays/easter/easter_riddle.py
+++ b/bot/exts/holidays/easter/easter_riddle.py
@@ -18,7 +18,7 @@ TIMELIMIT = 10
class EasterRiddle(commands.Cog):
- """This cog contains the command for the Easter quiz!"""
+ """The Easter quiz cog."""
def __init__(self, bot: Bot):
self.bot = bot
diff --git a/bot/exts/holidays/easter/egg_decorating.py b/bot/exts/holidays/easter/egg_decorating.py
index a9334820..1327f4d0 100644
--- a/bot/exts/holidays/easter/egg_decorating.py
+++ b/bot/exts/holidays/easter/egg_decorating.py
@@ -4,7 +4,6 @@ import random
from contextlib import suppress
from io import BytesIO
from pathlib import Path
-from typing import Optional, Union
import discord
from PIL import Image
@@ -33,7 +32,7 @@ class EggDecorating(commands.Cog):
"""Decorate some easter eggs!"""
@staticmethod
- def replace_invalid(colour: str) -> Optional[int]:
+ def replace_invalid(colour: str) -> int | None:
"""Attempts to match with HTML or XKCD colour names, returning the int value."""
with suppress(KeyError):
return int(HTML_COLOURS[colour], 16)
@@ -43,8 +42,8 @@ class EggDecorating(commands.Cog):
@commands.command(aliases=("decorateegg",))
async def eggdecorate(
- self, ctx: commands.Context, *colours: Union[discord.Colour, str]
- ) -> Optional[Image.Image]:
+ self, ctx: commands.Context, *colours: discord.Colour | str
+ ) -> Image.Image | None:
"""
Picks a random egg design and decorates it using the given colours.
@@ -53,7 +52,7 @@ class EggDecorating(commands.Cog):
"""
if len(colours) < 2:
await ctx.send("You must include at least 2 colours!")
- return
+ return None
invalid = []
colours = list(colours)
@@ -68,10 +67,10 @@ class EggDecorating(commands.Cog):
if len(invalid) > 1:
await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
- return
- elif len(invalid) == 1:
+ return None
+ if len(invalid) == 1:
await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
- return
+ return None
async with ctx.typing():
# Expand list to 8 colours
diff --git a/bot/exts/holidays/easter/egghead_quiz.py b/bot/exts/holidays/easter/egghead_quiz.py
index 2e4d1931..046f9fac 100644
--- a/bot/exts/holidays/easter/egghead_quiz.py
+++ b/bot/exts/holidays/easter/egghead_quiz.py
@@ -3,7 +3,6 @@ import logging
import random
from json import loads
from pathlib import Path
-from typing import Union
import discord
from discord.ext import commands
@@ -29,7 +28,7 @@ TIMELIMIT = 30
class EggheadQuiz(commands.Cog):
- """This cog contains the command for the Easter quiz!"""
+ """The Egghead quiz cog."""
def __init__(self):
self.quiz_messages = {}
@@ -117,9 +116,10 @@ class EggheadQuiz(commands.Cog):
)
await ctx.send(content, embed=a_embed)
+ return None
@staticmethod
- async def already_reacted(new_reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> bool:
+ async def already_reacted(new_reaction: discord.Reaction, user: discord.Member | discord.User) -> bool:
"""Returns whether a given user has reacted more than once to a given message."""
message = new_reaction.message
for reaction in message.reactions:
@@ -131,16 +131,17 @@ class EggheadQuiz(commands.Cog):
return False
@commands.Cog.listener()
- async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> None:
+ async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member | discord.User) -> None:
"""Listener to listen specifically for reactions of quiz messages."""
if user.bot:
- return
+ return None
if reaction.message.id not in self.quiz_messages:
- return
+ return None
if str(reaction.emoji) not in self.quiz_messages[reaction.message.id]:
return await reaction.message.remove_reaction(reaction, user)
if await self.already_reacted(reaction, user):
return await reaction.message.remove_reaction(reaction, user)
+ return None
async def setup(bot: Bot) -> None:
diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py
index 683114f9..ca68888b 100644
--- a/bot/exts/holidays/halloween/candy_collection.py
+++ b/bot/exts/holidays/halloween/candy_collection.py
@@ -1,6 +1,5 @@
import logging
import random
-from typing import Union
import discord
from async_rediscache import RedisCache
@@ -18,17 +17,17 @@ ADD_CANDY_EXISTING_REACTION_CHANCE = 10 # 10%
ADD_SKULL_REACTION_CHANCE = 50 # 2%
ADD_SKULL_EXISTING_REACTION_CHANCE = 20 # 5%
-EMOJIS = dict(
- CANDY="\N{CANDY}",
- SKULL="\N{SKULL}",
- MEDALS=(
+EMOJIS = {
+ "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}",
),
-)
+}
class CandyCollection(commands.Cog):
@@ -69,7 +68,7 @@ class CandyCollection(commands.Cog):
@in_month(Month.OCTOBER)
@commands.Cog.listener()
- async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.User, discord.Member]) -> None:
+ async def on_reaction_add(self, reaction: discord.Reaction, user: 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
@@ -140,7 +139,7 @@ class CandyCollection(commands.Cog):
@staticmethod
async def send_spook_msg(
- author: discord.Member, channel: discord.TextChannel, candies: Union[str, int]
+ author: discord.Member, channel: discord.TextChannel, candies: str | int
) -> None:
"""Send a spooky message."""
e = discord.Embed(colour=author.colour)
diff --git a/bot/exts/holidays/halloween/8ball.py b/bot/exts/holidays/halloween/eight_ball.py
index 21b55a01..21b55a01 100644
--- a/bot/exts/holidays/halloween/8ball.py
+++ b/bot/exts/holidays/halloween/eight_ball.py
diff --git a/bot/exts/holidays/halloween/monstersurvey.py b/bot/exts/holidays/halloween/monstersurvey.py
index 517f1bcb..d129f3cc 100644
--- a/bot/exts/holidays/halloween/monstersurvey.py
+++ b/bot/exts/holidays/halloween/monstersurvey.py
@@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Cog, Context
log = logging.getLogger(__name__)
EMOJIS = {
- "SUCCESS": u"\u2705",
- "ERROR": u"\u274C"
+ "SUCCESS": "\u2705",
+ "ERROR": "\u274C"
}
diff --git a/bot/exts/holidays/halloween/scarymovie.py b/bot/exts/holidays/halloween/scarymovie.py
index 9f1a95fd..fbab3dd2 100644
--- a/bot/exts/holidays/halloween/scarymovie.py
+++ b/bot/exts/holidays/halloween/scarymovie.py
@@ -36,7 +36,7 @@ class ScaryMovie(commands.Cog):
"""Selects a random movie and returns a JSON of movie details from TMDb."""
url = "https://api.themoviedb.org/3/discover/movie"
params = {
- "api_key": Tokens.tmdb,
+ "api_key": Tokens.tmdb.get_secret_value(),
"with_genres": "27",
"vote_count.gte": "5",
"include_adult": "false"
@@ -65,7 +65,7 @@ class ScaryMovie(commands.Cog):
# 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": Tokens.tmdb, "append_to_response": "credits"}
+ params={"api_key": Tokens.tmdb.get_secret_value(), "append_to_response": "credits"}
) as selection:
return await selection.json()
@@ -135,4 +135,7 @@ class ScaryMovie(commands.Cog):
async def setup(bot: Bot) -> None:
"""Load the Scary Movie Cog."""
+ if not Tokens.tmdb:
+ log.warning("No TMDB Token. Not loading ScaryMovie Cog.")
+ return
await bot.add_cog(ScaryMovie(bot))
diff --git a/bot/exts/holidays/halloween/spookygif.py b/bot/exts/holidays/halloween/spookygif.py
index 750e86ca..b3f9d703 100644
--- a/bot/exts/holidays/halloween/spookygif.py
+++ b/bot/exts/holidays/halloween/spookygif.py
@@ -21,7 +21,7 @@ class SpookyGif(commands.Cog):
async def spookygif(self, ctx: commands.Context) -> None:
"""Fetches a random gif from the GIPHY API and responds with it."""
async with ctx.typing():
- params = {"api_key": Tokens.giphy, "tag": "halloween", "rating": "g"}
+ params = {"api_key": Tokens.giphy.get_secret_value(), "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()
@@ -35,4 +35,7 @@ class SpookyGif(commands.Cog):
async def setup(bot: Bot) -> None:
"""Spooky GIF Cog load."""
+ if not Tokens.giphy:
+ log.warning("No Giphy token. Not loading SpookyGif cog.")
+ return
await bot.add_cog(SpookyGif(bot))
diff --git a/bot/exts/holidays/halloween/spookynamerate.py b/bot/exts/holidays/halloween/spookynamerate.py
index a76e5e12..78e8be8e 100644
--- a/bot/exts/holidays/halloween/spookynamerate.py
+++ b/bot/exts/holidays/halloween/spookynamerate.py
@@ -2,11 +2,10 @@ import asyncio
import json
import random
from collections import defaultdict
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
from logging import getLogger
from os import getenv
from pathlib import Path
-from typing import Optional
from async_rediscache import RedisCache
from discord import Embed, Reaction, TextChannel, User
@@ -146,7 +145,7 @@ class SpookyNameRate(Cog):
)
return
- elif data["name"] == name:
+ if data["name"] == name:
await ctx.send("TOO LATE. Someone has already added this name.")
return
@@ -261,12 +260,8 @@ class SpookyNameRate(Cog):
winners = []
for i, winner in enumerate(winner_messages):
winners.append(winner)
- if len(winner_messages) > i + 1:
- if winner_messages[i + 1][1]["score"] != winner[1]["score"]:
- break
- elif len(winner_messages) == (i + 1) + 1: # The next element is the last element
- if winner_messages[i + 1][1]["score"] != winner[1]["score"]:
- break
+ if len(winner_messages) > i + 1 and winner_messages[i + 1][1]["score"] != winner[1]["score"]:
+ break
# one iteration is complete
await channel.send("Today's Spooky Name Rate Game ends now, and the winner(s) is(are)...")
@@ -313,7 +308,7 @@ class SpookyNameRate(Cog):
if SpookyNameRate.debug:
return
- now = datetime.utcnow()
+ now = datetime.now(tz=UTC)
if now.hour < 12:
twelve_pm = now.replace(hour=12, minute=0, second=0, microsecond=0)
time_left = twelve_pm - now
@@ -353,7 +348,7 @@ class SpookyNameRate(Cog):
return embed
- async def get_channel(self) -> Optional[TextChannel]:
+ async def get_channel(self) -> TextChannel | None:
"""Gets the sir-lancebot-channel after waiting until ready."""
channel = self.bot.get_channel(
Channels.sir_lancebot_playground
@@ -369,7 +364,7 @@ class SpookyNameRate(Cog):
return True
if not Client.month_override:
- return datetime.utcnow().month == Month.OCTOBER
+ return datetime.now(tz=UTC).month == Month.OCTOBER
return Client.month_override == Month.OCTOBER
def cog_check(self, ctx: Context) -> bool:
diff --git a/bot/exts/holidays/hanukkah/hanukkah_embed.py b/bot/exts/holidays/hanukkah/hanukkah_embed.py
index 1ebc21e8..fd8edca1 100644
--- a/bot/exts/holidays/hanukkah/hanukkah_embed.py
+++ b/bot/exts/holidays/hanukkah/hanukkah_embed.py
@@ -1,5 +1,5 @@
-import datetime
import logging
+from datetime import UTC, date, datetime
from discord import Embed
from discord.ext import commands
@@ -21,18 +21,18 @@ class HanukkahEmbed(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.hanukkah_dates: list[datetime.date] = []
+ self.hanukkah_dates: list[date] = []
- def _parse_time_to_datetime(self, date: list[str]) -> datetime.datetime:
+ def _parse_time_to_datetime(self, date: list[str]) -> datetime:
"""Format the times provided by the api to datetime forms."""
try:
- return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z")
+ return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z")
except ValueError:
# there is a possibility of an event not having a time, just a day
# to catch this, we try again without time information
- return datetime.datetime.strptime(date, "%Y-%m-%d")
+ return datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC)
- async def fetch_hanukkah_dates(self) -> list[datetime.date]:
+ async def fetch_hanukkah_dates(self) -> list[date]:
"""Gets the dates for hanukkah festival."""
# clear the datetime objects to prevent a memory link
self.hanukkah_dates = []
@@ -52,11 +52,11 @@ class HanukkahEmbed(commands.Cog):
hanukkah_dates = await self.fetch_hanukkah_dates()
start_day = hanukkah_dates[0]
end_day = hanukkah_dates[-1]
- today = datetime.date.today()
+ today = datetime.now(tz=UTC).date()
embed = Embed(title="Hanukkah", colour=Colours.blue)
if start_day <= today <= end_day:
if start_day == today:
- now = datetime.datetime.utcnow()
+ now = datetime.now(tz=UTC)
hours = now.hour + 4 # using only hours
hanukkah_start_hour = 18
if hours < hanukkah_start_hour:
@@ -66,7 +66,8 @@ class HanukkahEmbed(commands.Cog):
)
await ctx.send(embed=embed)
return
- elif hours > hanukkah_start_hour:
+
+ if hours > hanukkah_start_hour:
embed.description = (
"It is the starting day of Hanukkah! "
f"Its been {hours - hanukkah_start_hour} hours hanukkah started!"
diff --git a/bot/exts/holidays/holidayreact.py b/bot/exts/holidays/holidayreact.py
index ee20b792..c3686fab 100644
--- a/bot/exts/holidays/holidayreact.py
+++ b/bot/exts/holidays/holidayreact.py
@@ -72,9 +72,9 @@ HOLIDAYS_TO_REACT = [
Valentines, Easter, EarthDay, Pride, Halloween, Hanukkah, Christmas
]
# Type (or order) doesn't matter here - set is for de-duplication
-MONTHS_TO_REACT = set(
+MONTHS_TO_REACT = {
month for holiday in HOLIDAYS_TO_REACT for month in holiday.months
-)
+}
class HolidayReact(Cog):
diff --git a/bot/exts/holidays/pride/pride_anthem.py b/bot/exts/holidays/pride/pride_anthem.py
index 6b78cba1..c719e388 100644
--- a/bot/exts/holidays/pride/pride_anthem.py
+++ b/bot/exts/holidays/pride/pride_anthem.py
@@ -2,7 +2,6 @@ import json
import logging
import random
from pathlib import Path
-from typing import Optional
from discord.ext import commands
@@ -16,7 +15,7 @@ VIDEOS = json.loads(Path("bot/resources/holidays/pride/anthems.json").read_text(
class PrideAnthem(commands.Cog):
"""Embed a random youtube video for a gay anthem!"""
- def get_video(self, genre: Optional[str] = None) -> dict:
+ def get_video(self, genre: str | None = None) -> dict:
"""
Picks a random anthem from the list.
@@ -25,12 +24,12 @@ class PrideAnthem(commands.Cog):
"""
if not genre:
return random.choice(VIDEOS)
- else:
- 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.")
+
+ 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.")
@commands.command(name="prideanthem", aliases=("anthem", "pridesong"))
async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None:
diff --git a/bot/exts/holidays/pride/pride_facts.py b/bot/exts/holidays/pride/pride_facts.py
index 36a9415e..f3ce50a9 100644
--- a/bot/exts/holidays/pride/pride_facts.py
+++ b/bot/exts/holidays/pride/pride_facts.py
@@ -1,9 +1,8 @@
import json
import logging
import random
-from datetime import datetime
+from datetime import UTC, datetime
from pathlib import Path
-from typing import Union
import dateutil.parser
import discord
@@ -29,11 +28,11 @@ class PrideFacts(commands.Cog):
async def send_pride_fact_daily(self) -> None:
"""Background task to post the daily pride fact every day."""
channel = self.bot.get_channel(Channels.sir_lancebot_playground)
- await self.send_select_fact(channel, datetime.utcnow())
+ await self.send_select_fact(channel, datetime.now(tz=UTC))
async def send_random_fact(self, ctx: commands.Context) -> None:
"""Provides a fact from any previous day, or today."""
- now = datetime.utcnow()
+ now = datetime.now(tz=UTC)
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]
@@ -42,9 +41,9 @@ class PrideFacts(commands.Cog):
except IndexError:
await ctx.send("No facts available")
- async def send_select_fact(self, target: discord.abc.Messageable, _date: Union[str, datetime]) -> None:
+ async def send_select_fact(self, target: discord.abc.Messageable, _date: str | datetime) -> None:
"""Provides the fact for the specified day, if the day is today, or is in the past."""
- now = datetime.utcnow()
+ now = datetime.now(tz=UTC)
if isinstance(_date, str):
try:
date = dateutil.parser.parse(_date, dayfirst=False, yearfirst=False, fuzzy=True)
@@ -76,7 +75,7 @@ class PrideFacts(commands.Cog):
will be provided.
"""
if not option:
- await self.send_select_fact(ctx, datetime.utcnow())
+ await self.send_select_fact(ctx, datetime.now(tz=UTC))
elif option.lower().startswith("rand"):
await self.send_random_fact(ctx)
else:
diff --git a/bot/exts/holidays/pride/pride_leader.py b/bot/exts/holidays/pride/pride_leader.py
index 120e9e16..b4a98892 100644
--- a/bot/exts/holidays/pride/pride_leader.py
+++ b/bot/exts/holidays/pride/pride_leader.py
@@ -2,7 +2,6 @@ import json
import logging
import random
from pathlib import Path
-from typing import Optional
import discord
from discord.ext import commands
@@ -90,7 +89,7 @@ class PrideLeader(commands.Cog):
return embed
@commands.command(aliases=("pl", "prideleader"))
- async def pride_leader(self, ctx: commands.Context, *, pride_leader_name: Optional[str]) -> None:
+ async def pride_leader(self, ctx: commands.Context, *, pride_leader_name: str | None) -> None:
"""
Information about a Pride Leader.
diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py
index 5ffd14e6..c2dd8bb6 100644
--- a/bot/exts/holidays/valentines/be_my_valentine.py
+++ b/bot/exts/holidays/valentines/be_my_valentine.py
@@ -7,7 +7,7 @@ import discord
from discord.ext import commands
from bot.bot import Bot
-from bot.constants import Channels, Colours, Lovefest, Month, PYTHON_PREFIX
+from bot.constants import Channels, Colours, Month, PYTHON_PREFIX, Roles
from bot.utils.decorators import in_month
from bot.utils.exceptions import MovedCommandError
@@ -60,7 +60,7 @@ class BeMyValentine(commands.Cog):
# This command should only be used in the server
raise commands.UserInputError("You are supposed to use this command in the server.")
- if Lovefest.role_id not in [role.id for role in user.roles]:
+ if Roles.lovefest not in [role.id for role in user.roles]:
raise commands.UserInputError(
f"You cannot send a valentine to {user} as they do not have the lovefest role!"
)
@@ -95,7 +95,7 @@ class BeMyValentine(commands.Cog):
example : .bemyvalentine secret Iceman#6508 Hey I love you, wanna hang around ? (sends the custom message to
Iceman in DM making you anonymous)
"""
- if Lovefest.role_id not in [role.id for role in user.roles]:
+ if Roles.lovefest not in [role.id for role in user.roles]:
await ctx.message.delete()
raise commands.UserInputError(
f"You cannot send a valentine to {user} as they do not have the lovefest role!"
@@ -126,15 +126,14 @@ class BeMyValentine(commands.Cog):
if valentine_type is None:
return self.random_valentine()
- elif valentine_type.lower() in ["p", "poem"]:
+ if valentine_type.lower() in ["p", "poem"]:
return self.valentine_poem(), "A poem dedicated to"
- elif valentine_type.lower() in ["c", "compliment"]:
+ if 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.
- return valentine_type, "A message for"
+ # in this case, the user decides to type his own valentine.
+ return valentine_type, "A message for"
@staticmethod
def random_emoji() -> tuple[str, str]:
@@ -148,10 +147,7 @@ class BeMyValentine(commands.Cog):
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"
- else:
- title = "A compliment for "
+ title = "A poem dedicated to" if random_valentine == valentine_poem else "A compliment for "
return random_valentine, title
def valentine_poem(self) -> str:
diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py
index c212e833..9b363c5e 100644
--- a/bot/exts/holidays/valentines/lovecalculator.py
+++ b/bot/exts/holidays/valentines/lovecalculator.py
@@ -3,8 +3,8 @@ import hashlib
import json
import logging
import random
+from collections.abc import Coroutine
from pathlib import Path
-from typing import Coroutine, Optional
import discord
from discord import Member
@@ -12,7 +12,7 @@ from discord.ext import commands
from discord.ext.commands import BadArgument, Cog, clean_content
from bot.bot import Bot
-from bot.constants import Channels, Lovefest, Month, PYTHON_PREFIX
+from bot.constants import Channels, Month, PYTHON_PREFIX, Roles
from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ class LoveCalculator(Cog):
@in_month(Month.FEBRUARY)
@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: Member, whom: Optional[Member] = None) -> None:
+ async def love(self, ctx: commands.Context, who: Member, whom: Member | None = None) -> None:
"""
Tells you how much the two love each other.
@@ -45,8 +45,8 @@ class LoveCalculator(Cog):
Running .love @chrisjl#2655 @joe#6000 will yield the same result as before.
"""
if (
- Lovefest.role_id not in [role.id for role in who.roles]
- or (whom is not None and Lovefest.role_id not in [role.id for role in whom.roles])
+ Roles.lovefest not in [role.id for role in who.roles]
+ or (whom is not None and Roles.lovefest not in [role.id for role in whom.roles])
):
raise BadArgument(
"This command can only be ran against members with the lovefest role! "
diff --git a/bot/exts/holidays/valentines/myvalenstate.py b/bot/exts/holidays/valentines/myvalenstate.py
index 8d8772d4..fcef24bc 100644
--- a/bot/exts/holidays/valentines/myvalenstate.py
+++ b/bot/exts/holidays/valentines/myvalenstate.py
@@ -47,7 +47,7 @@ class MyValenstate(commands.Cog):
else:
author = name.lower().replace(" ", "")
- for state in STATES.keys():
+ for state in STATES:
lower_state = state.lower().replace(" ", "")
eq_chars[state] = self.levenshtein(author, lower_state)
@@ -64,8 +64,7 @@ class MyValenstate(commands.Cog):
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_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",
diff --git a/bot/exts/holidays/valentines/valentine_zodiac.py b/bot/exts/holidays/valentines/valentine_zodiac.py
index 0a28a5c5..f0c8978d 100644
--- a/bot/exts/holidays/valentines/valentine_zodiac.py
+++ b/bot/exts/holidays/valentines/valentine_zodiac.py
@@ -2,9 +2,8 @@ import calendar
import json
import logging
import random
-from datetime import datetime
+from datetime import UTC, datetime
from pathlib import Path
-from typing import Union
import discord
from discord.ext import commands
@@ -78,6 +77,7 @@ class ValentineZodiac(commands.Cog):
if zodiac_data["start_at"].date() <= query_date.date() <= zodiac_data["end_at"].date():
log.trace("Zodiac name sent.")
return zodiac_name
+ return None
@commands.group(name="zodiac", invoke_without_command=True)
async def zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None:
@@ -87,7 +87,7 @@ class ValentineZodiac(commands.Cog):
log.trace("Embed successfully sent.")
@zodiac.command(name="date")
- async def date_and_month(self, ctx: commands.Context, date: int, month: Union[int, str]) -> None:
+ async def date_and_month(self, ctx: commands.Context, date: int, month: int | str) -> None:
"""Provides information about zodiac sign by taking month and date as input."""
if isinstance(month, str):
month = month.capitalize()
@@ -103,7 +103,7 @@ class ValentineZodiac(commands.Cog):
final_embed = self.zodiac_build_embed(zodiac)
else:
try:
- zodiac_sign_based_on_date = self.zodiac_date_verifier(datetime(2020, month, date))
+ zodiac_sign_based_on_date = self.zodiac_date_verifier(datetime(2020, month, date, tzinfo=UTC))
log.trace("zodiac sign based on month and date received.")
except ValueError as e:
final_embed = discord.Embed()
diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py
index 0b7ce429..a2d1d1d4 100644
--- a/bot/exts/utilities/bookmark.py
+++ b/bot/exts/utilities/bookmark.py
@@ -1,6 +1,5 @@
import logging
import random
-from typing import Optional
import discord
from discord.ext import commands
@@ -234,15 +233,15 @@ class Bookmark(commands.Cog):
This command allows deleting any message sent by Sir-Lancebot in the user's DM channel with the bot.
The command invocation must be a reply to the message that is to be deleted.
"""
- target_message: Optional[discord.Message] = getattr(ctx.message.reference, "resolved", None)
+ target_message: discord.Message | None = getattr(ctx.message.reference, "resolved", None)
if target_message is None:
raise commands.UserInputError("You must reply to the message from Sir-Lancebot you wish to delete.")
if not isinstance(ctx.channel, discord.DMChannel):
raise commands.UserInputError("You can only run this command your own DMs!")
- elif target_message.channel != ctx.channel:
+ if target_message.channel != ctx.channel:
raise commands.UserInputError("You can only delete messages in your own DMs!")
- elif target_message.author != self.bot.user:
+ if target_message.author != self.bot.user:
raise commands.UserInputError("You can only delete messages sent by Sir Lancebot!")
await target_message.delete()
diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py
index 46bc0fae..2f9ac73e 100644
--- a/bot/exts/utilities/challenges.py
+++ b/bot/exts/utilities/challenges.py
@@ -1,7 +1,6 @@
import logging
from asyncio import to_thread
from random import choice
-from typing import Union
from bs4 import BeautifulSoup
from discord import Embed, Interaction, SelectOption, ui
@@ -58,7 +57,7 @@ class InformationDropdown(ui.Select):
SelectOption(
label="Other Information",
description="See how other people performed on this kata and more!",
- emoji="ℹ"
+ emoji="🇮"
)
]
@@ -101,7 +100,7 @@ class Challenges(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- async def kata_id(self, search_link: str, params: dict) -> Union[str, Embed]:
+ async def kata_id(self, search_link: str, params: dict) -> str | Embed:
"""
Uses bs4 to get the HTML code for the page of katas, where the page is the link of the formatted `search_link`.
@@ -123,14 +122,13 @@ class Challenges(commands.Cog):
if not first_kata_div:
raise commands.BadArgument("No katas could be found with the filters provided.")
- else:
- first_kata_div = choice(first_kata_div)
# There are numerous divs before arriving at the id of the kata, which can be used for the link.
+ first_kata_div = choice(first_kata_div)
first_kata_id = first_kata_div.a["href"].split("/")[-1]
return first_kata_id
- async def kata_information(self, kata_id: str) -> Union[dict, Embed]:
+ async def kata_information(self, kata_id: str) -> dict | Embed:
"""
Returns the information about the Kata.
diff --git a/bot/exts/utilities/cheatsheet.py b/bot/exts/utilities/cheatsheet.py
index 3141a050..98ce3f57 100644
--- a/bot/exts/utilities/cheatsheet.py
+++ b/bot/exts/utilities/cheatsheet.py
@@ -1,6 +1,5 @@
import random
import re
-from typing import Union
from urllib.parse import quote_plus
from discord import Embed
@@ -52,7 +51,7 @@ class CheatSheet(commands.Cog):
)
return embed
- def result_fmt(self, url: str, body_text: str) -> tuple[bool, Union[str, Embed]]:
+ def result_fmt(self, url: str, body_text: str) -> tuple[bool, str | Embed]:
"""Format Result."""
if body_text.startswith("# 404 NOT FOUND"):
embed = self.fmt_error_embed()
@@ -80,7 +79,7 @@ class CheatSheet(commands.Cog):
aliases=("cht.sh", "cheatsheet", "cheat-sheet", "cht"),
)
@commands.cooldown(1, 10, BucketType.user)
- @whitelist_override(categories=[Categories.help_in_use])
+ @whitelist_override(categories=[Categories.python_help_system])
async def cheat_sheet(self, ctx: Context, *search_terms: str) -> None:
"""
Search cheat.sh.
diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py
index 20f97e4b..b8f1d62d 100644
--- a/bot/exts/utilities/colour.py
+++ b/bot/exts/utilities/colour.py
@@ -4,7 +4,6 @@ import pathlib
import random
import string
from io import BytesIO
-from typing import Optional
import discord
import rapidfuzz
@@ -25,7 +24,7 @@ class Colour(commands.Cog):
self.bot = bot
with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f:
self.colour_mapping = json.load(f)
- del self.colour_mapping['_'] # Delete source credit entry
+ del self.colour_mapping["_"] # Delete source credit entry
async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None:
"""Create and send embed from user given colour information."""
@@ -84,7 +83,7 @@ class Colour(commands.Cog):
roles=constants.STAFF_ROLES,
categories=[constants.Categories.development, constants.Categories.media]
)
- async def colour(self, ctx: commands.Context, *, colour_input: Optional[str] = None) -> None:
+ async def colour(self, ctx: commands.Context, *, colour_input: str | None = None) -> None:
"""
Create an embed that displays colour information.
@@ -209,7 +208,7 @@ class Colour(commands.Cog):
def _rgb_to_hsl(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
"""Convert RGB values to HSL values."""
rgb_list = [val / 255.0 for val in rgb]
- h, l, s = colorsys.rgb_to_hls(*rgb_list)
+ h, l, s = colorsys.rgb_to_hls(*rgb_list) # noqa: E741
hsl = (round(h * 360), round(s * 100), round(l * 100))
return hsl
@@ -233,7 +232,7 @@ class Colour(commands.Cog):
hex_code = f"#{hex_}".upper()
return hex_code
- def _rgb_to_name(self, rgb: tuple[int, int, int]) -> Optional[str]:
+ def _rgb_to_name(self, rgb: tuple[int, int, int]) -> str | None:
"""Convert RGB values to a fuzzy matched name."""
input_hex_colour = self._rgb_to_hex(rgb)
try:
@@ -247,7 +246,7 @@ class Colour(commands.Cog):
colour_name = None
return colour_name
- def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Optional[str]:
+ def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> str | None:
"""Convert a colour name to HEX code."""
try:
match, certainty, _ = rapidfuzz.process.extractOne(
@@ -256,7 +255,7 @@ class Colour(commands.Cog):
score_cutoff=80
)
except (ValueError, TypeError):
- return
+ return None
return f"#{self.colour_mapping[match]}"
diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py
index 410ea884..a019c789 100644
--- a/bot/exts/utilities/conversationstarters.py
+++ b/bot/exts/utilities/conversationstarters.py
@@ -2,7 +2,6 @@ import asyncio
from contextlib import suppress
from functools import partial
from pathlib import Path
-from typing import Union
import discord
import yaml
@@ -16,11 +15,11 @@ from bot.utils.randomization import RandomCycle
SUGGESTION_FORM = "https://forms.gle/zw6kkJqv8U43Nfjg9"
with Path("bot/resources/utilities/starter.yaml").open("r", encoding="utf8") as f:
- STARTERS = yaml.load(f, Loader=yaml.FullLoader)
+ STARTERS = yaml.safe_load(f)
with Path("bot/resources/utilities/py_topics.yaml").open("r", encoding="utf8") as f:
# First ID is #python-general and the rest are top to bottom categories of Topical Chat/Help.
- PY_TOPICS = yaml.load(f, Loader=yaml.FullLoader)
+ PY_TOPICS = yaml.safe_load(f)
# Removing `None` from lists of topics, if not a list, it is changed to an empty one.
PY_TOPICS = {k: [i for i in v if i] if isinstance(v, list) else [] for k, v in PY_TOPICS.items()}
@@ -67,7 +66,7 @@ class ConvoStarters(commands.Cog):
@staticmethod
def _predicate(
- command_invoker: Union[discord.User, discord.Member],
+ command_invoker: discord.User | discord.Member,
message: discord.Message,
reaction: discord.Reaction,
user: discord.User
@@ -84,7 +83,7 @@ class ConvoStarters(commands.Cog):
async def _listen_for_refresh(
self,
- command_invoker: Union[discord.User, discord.Member],
+ command_invoker: discord.User | discord.Member,
message: discord.Message
) -> None:
await message.add_reaction("🔄")
diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py
index ec40be01..ce352fe2 100644
--- a/bot/exts/utilities/emoji.py
+++ b/bot/exts/utilities/emoji.py
@@ -2,8 +2,7 @@ import logging
import random
import textwrap
from collections import defaultdict
-from datetime import datetime
-from typing import Optional
+from datetime import UTC, datetime
from discord import Color, Embed, Emoji
from discord.ext import commands
@@ -28,7 +27,7 @@ class Emojis(commands.Cog):
embed = Embed(
color=Colours.orange,
title="Emoji Count",
- timestamp=datetime.utcnow()
+ timestamp=datetime.now(tz=UTC)
)
msg = []
@@ -71,7 +70,7 @@ class Emojis(commands.Cog):
return embed, msg
@commands.group(name="emoji", invoke_without_command=True)
- async def emoji_group(self, ctx: commands.Context, emoji: Optional[Emoji]) -> None:
+ async def emoji_group(self, ctx: commands.Context, emoji: Emoji | None) -> None:
"""A group of commands related to emojis."""
if emoji is not None:
await ctx.invoke(self.info_command, emoji)
diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py
index 6f572640..d1ba98bb 100644
--- a/bot/exts/utilities/epoch.py
+++ b/bot/exts/utilities/epoch.py
@@ -1,4 +1,5 @@
-from typing import Optional, Union
+
+import contextlib
import arrow
import discord
@@ -24,7 +25,7 @@ DROPDOWN_TIMEOUT = 60
class DateString(commands.Converter):
"""Convert a relative or absolute date/time string to an arrow.Arrow object."""
- async def convert(self, ctx: commands.Context, argument: str) -> Union[arrow.Arrow, Optional[tuple]]:
+ async def convert(self, ctx: commands.Context, argument: str) -> arrow.Arrow | tuple | None:
"""
Convert a relative or absolute date/time string to an arrow.Arrow object.
@@ -88,10 +89,8 @@ class Epoch(commands.Cog):
view = TimestampMenuView(ctx, self._format_dates(date_time), epoch)
original = await ctx.send(f"`{epoch}`", view=view)
await view.wait() # wait until expiration before removing the dropdown
- try:
+ with contextlib.suppress(discord.NotFound):
await original.edit(view=None)
- except discord.NotFound: # disregard the error message if the message is deleled
- pass
@staticmethod
def _format_dates(date: arrow.Arrow) -> list[str]:
@@ -100,7 +99,7 @@ class Epoch(commands.Cog):
These are used in the description of each style in the dropdown
"""
- date = date.to('utc')
+ date = date.to("utc")
formatted = [str(int(date.timestamp()))]
formatted += [date.format(format[1]) for format in list(STYLES.values())[1:7]]
formatted.append(date.humanize())
@@ -115,7 +114,7 @@ class TimestampMenuView(discord.ui.View):
self.ctx = ctx
self.epoch = epoch
self.dropdown: discord.ui.Select = self.children[0]
- for label, date_time in zip(STYLES.keys(), formatted_times):
+ for label, date_time in zip(STYLES.keys(), formatted_times, strict=True):
self.dropdown.add_option(label=label, description=date_time)
@discord.ui.select(placeholder="Select the format of your timestamp")
diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py
index a7979718..74120f2d 100644
--- a/bot/exts/utilities/githubinfo.py
+++ b/bot/exts/utilities/githubinfo.py
@@ -1,9 +1,8 @@
import logging
import random
import re
-import typing as t
from dataclasses import dataclass
-from datetime import datetime
+from datetime import UTC, datetime
from urllib.parse import quote
import discord
@@ -26,7 +25,7 @@ ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{numbe
PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}"
if Tokens.github:
- REQUEST_HEADERS["Authorization"] = f"token {Tokens.github}"
+ REQUEST_HEADERS["Authorization"] = f"token {Tokens.github.get_secret_value()}"
CODE_BLOCK_RE = re.compile(
r"^`([^`\n]+)`" # Inline codeblock
@@ -48,7 +47,7 @@ AUTOMATIC_REGEX = re.compile(
class FoundIssue:
"""Dataclass representing an issue found by the regex."""
- organisation: t.Optional[str]
+ organisation: str | None
repository: str
number: str
@@ -89,7 +88,7 @@ class GithubInfo(commands.Cog):
number: int,
repository: str,
user: str
- ) -> t.Union[IssueState, FetchError]:
+ ) -> IssueState | FetchError:
"""
Retrieve an issue from a GitHub repository.
@@ -105,9 +104,9 @@ class GithubInfo(commands.Cog):
log.info(f"Ratelimit reached while fetching {url}")
return FetchError(403, "Ratelimit reached, please retry in a few minutes.")
return FetchError(403, "Cannot access issue.")
- elif r.status in (404, 410):
+ if r.status in (404, 410):
return FetchError(r.status, "Issue not found.")
- elif r.status != 200:
+ if r.status != 200:
return FetchError(r.status, "Error while fetching issue.")
# The initial API request is made to the issues API endpoint, which will return information
@@ -141,7 +140,7 @@ class GithubInfo(commands.Cog):
@staticmethod
def format_embed(
- results: t.List[t.Union[IssueState, FetchError]]
+ results: list[IssueState | FetchError]
) -> discord.Embed:
"""Take a list of IssueState or FetchError and format a Discord embed for them."""
description_list = []
@@ -261,7 +260,7 @@ class GithubInfo(commands.Cog):
description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "",
colour=discord.Colour.og_blurple(),
url=user_data["html_url"],
- timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ")
+ timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
)
embed.set_thumbnail(url=user_data["avatar_url"])
embed.set_footer(text="Account created at")
@@ -293,7 +292,7 @@ class GithubInfo(commands.Cog):
await ctx.send(embed=embed)
- @github_group.command(name='repository', aliases=('repo',))
+ @github_group.command(name="repository", aliases=("repo",))
async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None:
"""
Fetches a repositories' GitHub information.
@@ -347,8 +346,12 @@ 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"
+ ).replace(tzinfo=UTC).strftime("%d/%m/%Y")
+ last_pushed = datetime.strptime(
+ repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ"
+ ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M")
embed.set_footer(
text=(
diff --git a/bot/exts/utilities/realpython.py b/bot/exts/utilities/realpython.py
index 46b02866..c63fca85 100644
--- a/bot/exts/utilities/realpython.py
+++ b/bot/exts/utilities/realpython.py
@@ -1,6 +1,5 @@
import logging
from html import unescape
-from typing import Optional
from urllib.parse import quote_plus
from discord import Embed
@@ -31,8 +30,13 @@ class RealPython(commands.Cog):
@commands.command(aliases=["rp"])
@commands.cooldown(1, 10, commands.cooldowns.BucketType.user)
- async def realpython(self, ctx: commands.Context, amount: Optional[int] = 5, *,
- user_search: Optional[str] = None) -> None:
+ async def realpython(
+ self,
+ ctx: commands.Context,
+ amount: int | None = 5,
+ *,
+ user_search: str | None = None
+ ) -> None:
"""
Send some articles from RealPython that match the search terms.
diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py
index 028c16bc..f8e358de 100644
--- a/bot/exts/utilities/reddit.py
+++ b/bot/exts/utilities/reddit.py
@@ -3,8 +3,7 @@ import logging
import random
import textwrap
from collections import namedtuple
-from datetime import datetime, timedelta
-from typing import Union
+from datetime import UTC, datetime, timedelta
from aiohttp import BasicAuth, ClientError
from discord import Colour, Embed, TextChannel
@@ -36,15 +35,15 @@ class Reddit(Cog):
self.webhook = None
self.access_token = None
- self.client_auth = BasicAuth(RedditConfig.client_id, RedditConfig.secret)
+ self.client_auth = BasicAuth(RedditConfig.client_id.get_secret_value(), RedditConfig.secret.get_secret_value())
self.auto_poster_loop.start()
async def cog_unload(self) -> None:
"""Stop the loop task and revoke the access token when the cog is unloaded."""
self.auto_poster_loop.cancel()
- if self.access_token and self.access_token.expires_at > datetime.utcnow():
- asyncio.create_task(self.revoke_access_token())
+ if self.access_token and self.access_token.expires_at > datetime.now(tz=UTC):
+ await self.revoke_access_token()
async def cog_load(self) -> None:
"""Sets the reddit webhook when the cog is loaded."""
@@ -55,7 +54,7 @@ class Reddit(Cog):
"""Get the #reddit channel object from the bot's cache."""
return self.bot.get_channel(Channels.reddit)
- def build_pagination_pages(self, posts: list[dict], paginate: bool) -> Union[list[tuple], str]:
+ def build_pagination_pages(self, posts: list[dict], paginate: bool) -> list[tuple] | str:
"""Build embed pages required for Paginator."""
pages = []
first_page = ""
@@ -138,17 +137,15 @@ class Reddit(Cog):
expiration = int(content["expires_in"]) - 60 # Subtract 1 minute for leeway.
self.access_token = AccessToken(
token=content["access_token"],
- expires_at=datetime.utcnow() + timedelta(seconds=expiration)
+ expires_at=datetime.now(tz=UTC) + timedelta(seconds=expiration)
)
log.debug(f"New token acquired; expires on UTC {self.access_token.expires_at}")
return
- else:
- log.debug(
- f"Failed to get an access token: "
- f"status {response.status} & content type {response.content_type}; "
- f"retrying ({i}/{self.MAX_RETRIES})"
- )
+ log.debug(
+ f"Failed to get an access token: status {response.status} & content type {response.content_type}; "
+ f"retrying ({i}/{self.MAX_RETRIES})"
+ )
await asyncio.sleep(3)
@@ -183,7 +180,7 @@ class Reddit(Cog):
raise ValueError("Invalid amount of subreddit posts requested.")
# Renew the token if necessary.
- if not self.access_token or self.access_token.expires_at < datetime.utcnow():
+ if not self.access_token or self.access_token.expires_at < datetime.now(tz=UTC):
await self.get_access_token()
url = f"{self.OAUTH_URL}/{route}"
@@ -193,7 +190,7 @@ class Reddit(Cog):
headers={**self.HEADERS, "Authorization": f"bearer {self.access_token.token}"},
params=params
)
- if response.status == 200 and response.content_type == 'application/json':
+ if response.status == 200 and response.content_type == "application/json":
# Got appropriate response - process and return.
content = await response.json()
posts = content["data"]["children"]
@@ -205,11 +202,11 @@ class Reddit(Cog):
await asyncio.sleep(3)
log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}")
- return list() # Failed to get appropriate response within allowed number of retries.
+ return [] # Failed to get appropriate response within allowed number of retries.
async def get_top_posts(
self, subreddit: Subreddit, time: str = "all", amount: int = 5, paginate: bool = False
- ) -> Union[Embed, list[tuple]]:
+ ) -> Embed | list[tuple]:
"""
Get the top amount of posts for a given subreddit within a specified timeframe.
@@ -248,7 +245,7 @@ class Reddit(Cog):
"""Post the top 5 posts daily, and the top 5 posts weekly."""
# once d.py get support for `time` parameter in loop decorator,
# this can be removed and the loop can use the `time=datetime.time.min` parameter
- now = datetime.utcnow()
+ now = datetime.now(tz=UTC)
tomorrow = now + timedelta(days=1)
midnight_tomorrow = tomorrow.replace(hour=0, minute=0, second=0)
@@ -257,7 +254,7 @@ class Reddit(Cog):
if not self.webhook:
await self.bot.fetch_webhook(RedditConfig.webhook)
- if datetime.utcnow().weekday() == 0:
+ if datetime.now(tz=UTC).weekday() == 0:
await self.top_weekly_posts()
# if it's a monday send the top weekly posts
@@ -358,6 +355,6 @@ class Reddit(Cog):
async def setup(bot: Bot) -> None:
"""Load the Reddit cog."""
if not RedditConfig.secret or not RedditConfig.client_id:
- log.error("Credentials not provided, cog not loaded.")
+ log.warning("Credentials not provided, cog not loaded.")
return
await bot.add_cog(Reddit(bot))
diff --git a/bot/exts/utilities/stackoverflow.py b/bot/exts/utilities/stackoverflow.py
index b248e83f..1eeff45b 100644
--- a/bot/exts/utilities/stackoverflow.py
+++ b/bot/exts/utilities/stackoverflow.py
@@ -42,10 +42,10 @@ class Stackoverflow(commands.Cog):
if response.status == 200:
data = await response.json()
else:
- logger.error(f'Status code is not 200, it is {response.status}')
+ logger.error(f"Status code is not 200, it is {response.status}")
await ctx.send(embed=ERR_EMBED)
return
- if not data['items']:
+ if not data["items"]:
no_search_result = Embed(
title=f"No search results found for {search_query}",
color=Colours.soft_red
@@ -63,7 +63,7 @@ class Stackoverflow(commands.Cog):
)
for item in top5:
embed.add_field(
- name=unescape(item['title']),
+ name=unescape(item["title"]),
value=(
f"[{Emojis.reddit_upvote} {item['score']} "
f"{Emojis.stackoverflow_views} {item['view_count']} "
diff --git a/bot/exts/utilities/twemoji.py b/bot/exts/utilities/twemoji.py
index 25a03d25..a936f733 100644
--- a/bot/exts/utilities/twemoji.py
+++ b/bot/exts/utilities/twemoji.py
@@ -1,6 +1,6 @@
import logging
import re
-from typing import Literal, Optional
+from typing import Literal
import discord
from discord.ext import commands
@@ -57,7 +57,7 @@ class Twemoji(commands.Cog):
return embed
@staticmethod
- def emoji(codepoint: Optional[str]) -> Optional[str]:
+ def emoji(codepoint: str | None) -> str | None:
"""
Returns the emoji corresponding to a given `codepoint`, or `None` if no emoji was found.
@@ -66,9 +66,10 @@ class Twemoji(commands.Cog):
"""
if code := Twemoji.trim_code(codepoint):
return chr(int(code, 16))
+ return None
@staticmethod
- def codepoint(emoji: Optional[str]) -> Optional[str]:
+ def codepoint(emoji: str | None) -> str | None:
"""
Returns the codepoint, in a trimmed format, of a single emoji.
@@ -82,7 +83,7 @@ class Twemoji(commands.Cog):
return hex(ord(emoji)).removeprefix("0x")
@staticmethod
- def trim_code(codepoint: Optional[str]) -> Optional[str]:
+ def trim_code(codepoint: str | None) -> str | None:
"""
Returns the meaningful information from the given `codepoint`.
@@ -98,6 +99,7 @@ class Twemoji(commands.Cog):
"""
if code := CODEPOINT_REGEX.search(codepoint or ""):
return code.group()
+ return None
@staticmethod
def codepoint_from_input(raw_emoji: tuple[str, ...]) -> str:
diff --git a/bot/exts/utilities/wikipedia.py b/bot/exts/utilities/wikipedia.py
index d87982c9..d12cb19d 100644
--- a/bot/exts/utilities/wikipedia.py
+++ b/bot/exts/utilities/wikipedia.py
@@ -1,6 +1,6 @@
import logging
import re
-from datetime import datetime
+from datetime import UTC, datetime
from html import unescape
from discord import Color, Embed, TextChannel
@@ -85,7 +85,7 @@ class WikipediaSearch(commands.Cog):
colour=Color.og_blurple()
)
embed.set_thumbnail(url=WIKI_THUMBNAIL)
- embed.timestamp = datetime.utcnow()
+ embed.timestamp = datetime.now(tz=UTC)
await LinePaginator.paginate(contents, ctx, embed, restrict_to_user=ctx.author)
else:
await ctx.send(
diff --git a/bot/exts/utilities/wolfram.py b/bot/exts/utilities/wolfram.py
index a2f1228a..d5669c6b 100644
--- a/bot/exts/utilities/wolfram.py
+++ b/bot/exts/utilities/wolfram.py
@@ -1,6 +1,6 @@
import logging
+from collections.abc import Callable
from io import BytesIO
-from typing import Callable, Optional
from urllib.parse import urlencode
import arrow
@@ -10,12 +10,12 @@ 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.constants import Colours, STAFF_ROLES, Wolfram as WolframConfig
from bot.utils.pagination import ImagePaginator
log = logging.getLogger(__name__)
-APPID = Wolfram.key
+APPID = WolframConfig.key.get_secret_value()
DEFAULT_OUTPUT_FORMAT = "JSON"
QUERY = "http://api.wolframalpha.com/v2/{request}"
WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1"
@@ -23,10 +23,10 @@ WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1"
MAX_PODS = 20
# Allows for 10 wolfram calls pr user pr day
-usercd = commands.CooldownMapping.from_cooldown(Wolfram.user_limit_day, 60 * 60 * 24, BucketType.user)
+usercd = commands.CooldownMapping.from_cooldown(WolframConfig.user_limit_day, 60 * 60 * 24, BucketType.user)
# Allows for max api requests / days in month per day for the entire guild (Temporary)
-guildcd = commands.CooldownMapping.from_cooldown(Wolfram.guild_limit_day, 60 * 60 * 24, BucketType.guild)
+guildcd = commands.CooldownMapping.from_cooldown(WolframConfig.guild_limit_day, 60 * 60 * 24, BucketType.guild)
async def send_embed(
@@ -64,10 +64,10 @@ def custom_cooldown(*ignore: int) -> Callable:
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
+ guild_cooldown = guildcd.get_bucket(ctx.message).get_tokens() != 0 # if guild is on cooldown
# check the message is in a guild, and check user bucket if user is not ignored
if ctx.guild and not any(r.id in ignore for r in ctx.author.roles):
- return guild_cooldown and not usercd.get_bucket(ctx.message).get_tokens() == 0
+ return guild_cooldown and usercd.get_bucket(ctx.message).get_tokens() != 0
return guild_cooldown
user_bucket = usercd.get_bucket(ctx.message)
@@ -105,7 +105,7 @@ def custom_cooldown(*ignore: int) -> Callable:
return check(predicate)
-async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[list[tuple[str, str]]]:
+async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> list[tuple[str, str]] | None:
"""Get the Wolfram API pod pages for the provided query."""
async with ctx.typing():
params = {
@@ -253,10 +253,7 @@ class Wolfram(Cog):
if not pages:
return
- if len(pages) >= 2:
- page = pages[1]
- else:
- page = pages[0]
+ page = pages[1] if len(pages) >= 2 else pages[0]
await send_embed(ctx, page[0], colour=Colours.soft_orange, img_url=page[1])
@@ -303,4 +300,7 @@ class Wolfram(Cog):
async def setup(bot: Bot) -> None:
"""Load the Wolfram cog."""
+ if not WolframConfig.key:
+ log.warning("No Wolfram API Key was provided. Not loading Wolfram Cog.")
+ return
await bot.add_cog(Wolfram(bot))
diff --git a/bot/exts/utilities/wtf_python.py b/bot/exts/utilities/wtf_python.py
index 0c0375cb..29a9611c 100644
--- a/bot/exts/utilities/wtf_python.py
+++ b/bot/exts/utilities/wtf_python.py
@@ -1,7 +1,6 @@
import logging
import random
import re
-from typing import Optional
import rapidfuzz
from discord import Embed, File
@@ -67,7 +66,7 @@ class WTFPython(commands.Cog):
hyper_link = match[0].split("(")[1].replace(")", "")
self.headers[match[0]] = f"{BASE_URL}/{hyper_link}"
- def fuzzy_match_header(self, query: str) -> Optional[str]:
+ def fuzzy_match_header(self, query: str) -> str | None:
"""
Returns the fuzzy match of a query if its ratio is above "MINIMUM_CERTAINTY" else returns None.
@@ -79,7 +78,7 @@ class WTFPython(commands.Cog):
return match if certainty > MINIMUM_CERTAINTY else None
@commands.command(aliases=("wtf",))
- async def wtf_python(self, ctx: commands.Context, *, query: Optional[str] = None) -> None:
+ async def wtf_python(self, ctx: commands.Context, *, query: str | None = None) -> None:
"""
Search WTF Python repository.
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 91682dbc..ddc2d111 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -3,8 +3,7 @@ import contextlib
import re
import string
from collections.abc import Iterable
-from datetime import datetime
-from typing import Optional
+from datetime import UTC, datetime
import discord
from discord.ext.commands import BadArgument, Context
@@ -27,8 +26,7 @@ def resolve_current_month() -> Month:
"""
if Client.month_override is not None:
return Month(Client.month_override)
- else:
- return Month(datetime.utcnow().month)
+ return Month(datetime.now(tz=UTC).month)
async def disambiguate(
@@ -38,7 +36,7 @@ async def disambiguate(
timeout: float = 30,
entries_per_page: int = 20,
empty: bool = False,
- embed: Optional[discord.Embed] = None
+ embed: discord.Embed | None = None
) -> str:
"""
Has the user choose between multiple entries in case one could not be chosen automatically.
@@ -130,9 +128,9 @@ def replace_many(
assert var == "That WAS a sentence"
"""
if ignore_case:
- replacements = dict(
- (word.lower(), replacement) for word, replacement in replacements.items()
- )
+ replacements = {
+ word.lower(): replacement for word, replacement in replacements.items()
+ }
words_to_replace = sorted(replacements, key=lambda s: (-len(s), s))
@@ -152,10 +150,9 @@ def replace_many(
cleaned_word = word.translate(str.maketrans("", "", string.punctuation))
if cleaned_word.isupper():
return replacement.upper()
- elif cleaned_word[0].isupper():
+ if cleaned_word[0].isupper():
return replacement.capitalize()
- else:
- return replacement.lower()
+ return replacement.lower()
return regex.sub(_repl, sentence)
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
index 857b6746..f1c2b3ce 100644
--- a/bot/utils/checks.py
+++ b/bot/utils/checks.py
@@ -1,7 +1,6 @@
import datetime
import logging
-from collections.abc import Container, Iterable
-from typing import Callable, Optional
+from collections.abc import Callable, Container, Iterable
from discord.ext.commands import (
BucketType, CheckFailure, Cog, Command, CommandOnCooldown, Context, Cooldown, CooldownMapping
@@ -10,12 +9,13 @@ from discord.ext.commands import (
from bot import constants
log = logging.getLogger(__name__)
+CODEJAM_CATEGORY_NAME = "Code Jam"
class InWhitelistCheckFailure(CheckFailure):
"""Raised when the `in_whitelist` check fails."""
- def __init__(self, redirect_channel: Optional[int]):
+ def __init__(self, redirect_channel: int | None):
self.redirect_channel = redirect_channel
if redirect_channel:
@@ -33,7 +33,7 @@ def in_whitelist_check(
channels: Container[int] = (),
categories: Container[int] = (),
roles: Container[int] = (),
- redirect: Optional[int] = constants.Channels.sir_lancebot_playground,
+ redirect: int | None = constants.Channels.sir_lancebot_playground,
fail_silently: bool = False,
) -> bool:
"""
@@ -74,7 +74,7 @@ def in_whitelist_check(
return True
category = getattr(ctx_channel, "category", None)
- if category and category.name == constants.codejam_categories_name:
+ if category and category.name == CODEJAM_CATEGORY_NAME:
log.trace(f"{ctx.author} may use the `{ctx.command.name}` command as they are in a codejam team channel.")
return True
@@ -153,7 +153,7 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy
return
# Cooldown logic, taken from discord.py internals.
- current = ctx.message.created_at.replace(tzinfo=datetime.timezone.utc).timestamp()
+ current = ctx.message.created_at.replace(tzinfo=datetime.UTC).timestamp()
bucket = buckets.get_bucket(ctx.message)
retry_after = bucket.update_rate_limit(current)
if retry_after:
diff --git a/bot/utils/converters.py b/bot/utils/converters.py
index 7227a406..6111b87d 100644
--- a/bot/utils/converters.py
+++ b/bot/utils/converters.py
@@ -1,5 +1,4 @@
-from datetime import datetime
-from typing import Union
+from datetime import UTC, datetime
import discord
from discord.ext import commands
@@ -47,7 +46,7 @@ class CoordinateConverter(commands.Converter):
return x, y
-SourceType = Union[commands.Command, commands.Cog]
+SourceType = commands.Command | commands.Cog
class SourceConverter(commands.Converter):
@@ -73,12 +72,12 @@ 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]:
+ async def convert(ctx: commands.Context, argument: str) -> 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")
+ date = datetime.strptime(argument, "%Y-%m-%d").replace(tzinfo=UTC)
except ValueError:
raise commands.BadArgument(
f"Can't convert `{argument}` to `datetime` in format `YYYY-MM-DD` or `int` in SOL."
diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py
index 442eb841..1cbad504 100644
--- a/bot/utils/decorators.py
+++ b/bot/utils/decorators.py
@@ -3,9 +3,8 @@ import functools
import logging
import random
from asyncio import Lock
-from collections.abc import Container
+from collections.abc import Callable, Container
from functools import wraps
-from typing import Callable, Optional, Union
from weakref import WeakValueDictionary
from discord import Colour, Embed
@@ -24,16 +23,12 @@ log = logging.getLogger(__name__)
class InChannelCheckFailure(CheckFailure):
"""Check failure when the user runs a command in a non-whitelisted channel."""
- pass
-
class InMonthCheckFailure(CheckFailure):
"""Check failure for when a command is invoked outside of its allowed month."""
- pass
-
-def seasonal_task(*allowed_months: Month, sleep_time: Union[float, int] = ONE_DAY) -> Callable:
+def seasonal_task(*allowed_months: Month, sleep_time: float | int = ONE_DAY) -> Callable:
"""
Perform the decorated method periodically in `allowed_months`.
@@ -79,8 +74,8 @@ def in_month_listener(*allowed_months: Month) -> Callable:
if current_month in allowed_months:
# Propagate return value although it should always be None
return await listener(*args, **kwargs)
- else:
- log.debug(f"Guarded {listener.__qualname__} from invoking in {current_month!s}")
+ log.debug(f"Guarded {listener.__qualname__} from invoking in {current_month!s}")
+ return None
return guarded_listener
return decorator
@@ -101,8 +96,7 @@ def in_month_command(*allowed_months: Month) -> Callable:
)
if can_run:
return True
- else:
- raise InMonthCheckFailure(f"Command can only be used in {human_months(allowed_months)}")
+ raise InMonthCheckFailure(f"Command can only be used in {human_months(allowed_months)}")
return commands.check(predicate)
@@ -201,13 +195,13 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo
# Determine which command's overrides we will use. Group commands will
# inherit from their parents if they don't define their own overrides
- overridden_command: Optional[commands.Command] = None
+ overridden_command: commands.Command | None = None
for command in [ctx.command, *ctx.command.parents]:
if hasattr(command.callback, "override"):
overridden_command = command
break
if overridden_command is not None:
- log.debug(f'Command {overridden_command} has overrides')
+ log.debug(f"Command {overridden_command} has overrides")
if overridden_command is not ctx.command:
log.debug(
f"Command '{ctx.command.qualified_name}' inherited overrides "
@@ -319,7 +313,7 @@ def whitelist_override(bypass_defaults: bool = False, allow_dm: bool = False, **
return inner
-def locked() -> Optional[Callable]:
+def locked() -> Callable | None:
"""
Allows the user to only run one instance of the decorated command at a time.
@@ -327,11 +321,11 @@ def locked() -> Optional[Callable]:
This decorator has to go before (below) the `command` decorator.
"""
- def wrap(func: Callable) -> Optional[Callable]:
+ def wrap(func: Callable) -> Callable | None:
func.__locks = WeakValueDictionary()
@wraps(func)
- async def inner(self: Callable, ctx: Context, *args, **kwargs) -> Optional[Callable]:
+ async def inner(self: Callable, ctx: Context, *args, **kwargs) -> Callable | None:
lock = func.__locks.setdefault(ctx.author.id, Lock())
if lock.locked():
embed = Embed()
@@ -344,7 +338,7 @@ def locked() -> Optional[Callable]:
)
embed.title = random.choice(ERROR_REPLIES)
await ctx.send(embed=embed)
- return
+ return None
async with func.__locks.setdefault(ctx.author.id, Lock()):
return await func(self, ctx, *args, **kwargs)
diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py
index 3cd96325..b1a35e63 100644
--- a/bot/utils/exceptions.py
+++ b/bot/utils/exceptions.py
@@ -1,16 +1,13 @@
-from typing import Optional
class UserNotPlayingError(Exception):
"""Raised when users try to use game commands when they are not playing."""
- pass
-
class APIError(Exception):
"""Raised when an external API (eg. Wikipedia) returns an error response."""
- def __init__(self, api: str, status_code: int, error_msg: Optional[str] = None):
+ def __init__(self, api: str, status_code: int, error_msg: str | None = None):
super().__init__()
self.api = api
self.status_code = status_code
diff --git a/bot/utils/messages.py b/bot/utils/messages.py
index b0c95583..4fb0b39b 100644
--- a/bot/utils/messages.py
+++ b/bot/utils/messages.py
@@ -1,6 +1,7 @@
+import contextlib
import logging
import re
-from typing import Callable, Optional, Union
+from collections.abc import Callable
from discord import Embed, Message
from discord.ext import commands
@@ -9,39 +10,35 @@ from discord.ext.commands import Context, MessageConverter
log = logging.getLogger(__name__)
-def sub_clyde(username: Optional[str]) -> Optional[str]:
+def sub_clyde(username: str | None) -> str | None:
"""
- Replace "e"/"E" in any "clyde" in `username` with a Cyrillic "е"/"E" and return the new string.
+ Replace "e"/"E" in any "clyde" in `username` with a Cyrillic "е"/"Е" and return the new string.
Discord disallows "clyde" anywhere in the username for webhooks. It will return a 400.
Return None only if `username` is None.
- """
+ """ # noqa: RUF002
def replace_e(match: re.Match) -> str:
- char = "е" if match[2] == "e" else "Е"
+ char = "е" if match[2] == "e" else "Е" # noqa: RUF001
return match[1] + char
if username:
return re.sub(r"(clyd)(e)", replace_e, username, flags=re.I)
- else:
- return username # Empty string or None
+ return username # Empty string or None
-async def get_discord_message(ctx: Context, text: str) -> Union[Message, str]:
+async def get_discord_message(ctx: Context, text: str) -> Message | str:
"""
Attempts to convert a given `text` to a discord Message object and return it.
Conversion will succeed if given a discord Message ID or link.
Returns `text` if the conversion fails.
"""
- try:
+ with contextlib.suppress(commands.BadArgument):
text = await MessageConverter().convert(ctx, text)
- except commands.BadArgument:
- pass
-
return text
-async def get_text_and_embed(ctx: Context, text: str) -> tuple[str, Optional[Embed]]:
+async def get_text_and_embed(ctx: Context, text: str) -> tuple[str, Embed | None]:
"""
Attempts to extract the text and embed from a possible link to a discord Message.
@@ -52,7 +49,7 @@ async def get_text_and_embed(ctx: Context, text: str) -> tuple[str, Optional[Emb
str: If `text` is a valid discord Message, the contents of the message, else `text`.
Optional[Embed]: The embed if found in the valid Message, else None
"""
- embed: Optional[Embed] = None
+ embed: Embed | None = None
msg = await get_discord_message(ctx, text)
# Ensure the user has read permissions for the channel the message is in
diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py
index b291f7db..ef3186d0 100644
--- a/bot/utils/pagination.py
+++ b/bot/utils/pagination.py
@@ -1,7 +1,6 @@
import asyncio
import logging
from collections.abc import Iterable
-from typing import Optional
from discord import Embed, Member, Reaction
from discord.abc import User
@@ -29,10 +28,10 @@ class LinePaginator(Paginator):
def __init__(
self,
- prefix: str = '```',
- suffix: str = '```',
+ prefix: str = "```",
+ suffix: str = "```",
max_size: int = 2000,
- max_lines: Optional[int] = None,
+ max_lines: int | None = None,
linesep: str = "\n"
):
"""
@@ -87,11 +86,13 @@ class LinePaginator(Paginator):
self._count += 1
@classmethod
- async def paginate(cls, lines: Iterable[str], ctx: Context, embed: Embed,
- 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) -> None:
+ async def paginate(
+ cls, lines: Iterable[str], ctx: Context,
+ embed: Embed, prefix: str = "", suffix: str = "",
+ max_lines: int | None = 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
+ ) -> None:
"""
Use a paginator and set of reactions to provide pagination over a set of lines.
@@ -170,26 +171,26 @@ class LinePaginator(Paginator):
log.debug("There's less than two pages, so we won't paginate - sending single page on its own")
await ctx.send(embed=embed)
- return
+ return None
+
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
else:
- if footer_text:
- embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
- else:
- embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
- log.trace(f"Setting embed footer to '{embed.footer.text}'")
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ log.trace(f"Setting embed footer to '{embed.footer.text}'")
- if url:
- embed.url = url
- log.trace(f"Setting embed url to '{url}'")
+ if url:
+ embed.url = url
+ log.trace(f"Setting embed url to '{url}'")
- log.debug("Sending first page to channel...")
- message = await ctx.send(embed=embed)
+ log.debug("Sending first page to channel...")
+ message = await ctx.send(embed=embed)
log.debug("Adding emoji reactions to message...")
for emoji in PAGINATION_EMOJI:
# Add all the applicable emoji to the message
- log.trace(f"Adding reaction: {repr(emoji)}")
+ log.trace(f"Adding reaction: {emoji!r}")
await message.add_reaction(emoji)
while True:
@@ -270,6 +271,7 @@ class LinePaginator(Paginator):
log.debug("Ending pagination and clearing reactions...")
await message.clear_reactions()
+ return None
class ImagePaginator(Paginator):
@@ -358,7 +360,7 @@ class ImagePaginator(Paginator):
if len(paginator.pages) <= 1:
await ctx.send(embed=embed)
- return
+ return None
embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
message = await ctx.send(embed=embed)
@@ -431,3 +433,4 @@ class ImagePaginator(Paginator):
log.debug("Ending pagination and clearing reactions...")
await message.clear_reactions()
+ return None
diff --git a/bot/utils/randomization.py b/bot/utils/randomization.py
index c9eabbd2..1caff3fa 100644
--- a/bot/utils/randomization.py
+++ b/bot/utils/randomization.py
@@ -1,7 +1,9 @@
import itertools
import random
from collections.abc import Iterable
-from typing import Any
+from typing import TypeVar
+
+T = TypeVar("T")
class RandomCycle:
@@ -11,11 +13,11 @@ class RandomCycle:
The iterable is reshuffled after each full cycle.
"""
- def __init__(self, iterable: Iterable):
+ def __init__(self, iterable: Iterable[T]):
self.iterable = list(iterable)
self.index = itertools.cycle(range(len(iterable)))
- def __next__(self) -> Any:
+ def __next__(self) -> T:
idx = next(self.index)
if idx == 0:
diff --git a/bot/utils/time.py b/bot/utils/time.py
index fbf2fd21..66f9e7cb 100644
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -1,4 +1,4 @@
-import datetime
+from datetime import UTC, datetime
from dateutil.relativedelta import relativedelta
@@ -17,12 +17,11 @@ def _stringify_time_unit(value: int, unit: str) -> str:
"""
if unit == "seconds" and value == 0:
return "0 seconds"
- elif value == 1:
+ if value == 1:
return f"{value} {unit[:-1]}"
- elif value == 0:
+ if value == 0:
return f"less than a {unit[:-1]}"
- else:
- return f"{value} {unit}"
+ return f"{value} {unit}"
def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: int = 6) -> str:
@@ -69,14 +68,14 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:
return humanized
-def time_since(past_datetime: datetime.datetime, precision: str = "seconds", max_units: int = 6) -> str:
+def time_since(past_datetime: datetime, precision: str = "seconds", max_units: int = 6) -> str:
"""
Takes a datetime and returns a human-readable string that describes how long ago that datetime was.
precision specifies the smallest unit of time to include (e.g. "seconds", "minutes").
max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours).
"""
- now = datetime.datetime.utcnow()
+ now = datetime.now(tz=UTC)
delta = abs(relativedelta(now, past_datetime))
humanized = humanize_delta(delta, precision, max_units)
diff --git a/poetry.lock b/poetry.lock
index 2e3c6565..c904c95e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -187,22 +187,22 @@ files = [
[[package]]
name = "attrs"
-version = "22.2.0"
+version = "23.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
files = [
- {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
- {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
+ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+ {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
]
[package.extras]
-cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
-tests = ["attrs[tests-no-zope]", "zope.interface"]
-tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
[[package]]
name = "beautifulsoup4"
@@ -225,14 +225,14 @@ lxml = ["lxml"]
[[package]]
name = "certifi"
-version = "2022.12.7"
+version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
- {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
+ {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
+ {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
]
[[package]]
@@ -326,100 +326,87 @@ files = [
[[package]]
name = "charset-normalizer"
-version = "3.0.1"
+version = "3.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
-python-versions = "*"
-files = [
- {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"},
- {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"},
- {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"},
- {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"},
- {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"},
- {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"},
- {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"},
- {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"},
+python-versions = ">=3.7.0"
+files = [
+ {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
+ {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
+ {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
+ {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
+ {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
+ {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
+ {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
]
[[package]]
@@ -454,14 +441,14 @@ cron = ["capturer (>=2.4)"]
[[package]]
name = "discord-py"
-version = "2.2.2"
+version = "2.2.3"
description = "A Python wrapper for the Discord API"
category = "main"
optional = false
python-versions = ">=3.8.0"
files = [
- {file = "discord.py-2.2.2-py3-none-any.whl", hash = "sha256:38fc52a784727b8e5e5749267089400035b187a009028eddfabeb182abcc6d52"},
- {file = "discord.py-2.2.2.tar.gz", hash = "sha256:b9944056bcb5711b2d04088848fd004466cf117c15c84fa798bf55470f28275f"},
+ {file = "discord.py-2.2.3-py3-none-any.whl", hash = "sha256:792bdcfe71cfe013c446cf379b2e83e08b5050604322ad6fb592864e63511abd"},
+ {file = "discord.py-2.2.3.tar.gz", hash = "sha256:f9df95795c6f52c5db43b7ab43634993e12ef233288636a759166dd9c134d077"},
]
[package.dependencies]
@@ -513,19 +500,19 @@ files = [
[[package]]
name = "fakeredis"
-version = "2.9.2"
+version = "2.12.0"
description = "Fake implementation of redis API for testing purposes."
category = "main"
optional = false
-python-versions = ">=3.8,<4.0"
+python-versions = ">=3.7,<4.0"
files = [
- {file = "fakeredis-2.9.2-py3-none-any.whl", hash = "sha256:35962e0ded572302c4461ad1a40308259cc10b7c546b13228e6d7880bf7b74bb"},
- {file = "fakeredis-2.9.2.tar.gz", hash = "sha256:3bcc2b5c10d5e03cc5b46697d77c651f48391b506ff931576d2869c7549e2e25"},
+ {file = "fakeredis-2.12.0-py3-none-any.whl", hash = "sha256:13040c67c471edfa4aa941f023d04f0df05f092e81c9f42a7f91d8fe67612386"},
+ {file = "fakeredis-2.12.0.tar.gz", hash = "sha256:9c98be9f31d4b7d610e83ce3a8a3cb0434beedd16125f10475c618df4c52ed10"},
]
[package.dependencies]
lupa = {version = ">=1.14,<2.0", optional = true, markers = "extra == \"lua\""}
-redis = ">=4,<5"
+redis = ">=4"
sortedcontainers = ">=2.4,<3.0"
[package.extras]
@@ -534,150 +521,19 @@ lua = ["lupa (>=1.14,<2.0)"]
[[package]]
name = "filelock"
-version = "3.9.0"
+version = "3.12.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
- {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
-]
-
-[package.extras]
-docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
-testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
-
-[[package]]
-name = "flake8"
-version = "6.0.0"
-description = "the modular source code checker: pep8 pyflakes and co"
-category = "dev"
-optional = false
-python-versions = ">=3.8.1"
-files = [
- {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
- {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
-]
-
-[package.dependencies]
-mccabe = ">=0.7.0,<0.8.0"
-pycodestyle = ">=2.10.0,<2.11.0"
-pyflakes = ">=3.0.0,<3.1.0"
-
-[[package]]
-name = "flake8-annotations"
-version = "3.0.0"
-description = "Flake8 Type Annotation Checks"
-category = "dev"
-optional = false
-python-versions = ">=3.8.1,<4.0.0"
-files = [
- {file = "flake8_annotations-3.0.0-py3-none-any.whl", hash = "sha256:ea927d31016515e9aa6e256651d74baeeee6fa4ad3f8383715ec5c0460a4c225"},
- {file = "flake8_annotations-3.0.0.tar.gz", hash = "sha256:88c8b35a0db10b9a92be69ed3f81494509a18db1c3162551e57bc0fc35fab065"},
-]
-
-[package.dependencies]
-attrs = ">=21.4"
-flake8 = ">=5.0"
-
-[[package]]
-name = "flake8-bugbear"
-version = "23.3.23"
-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.8.1"
-files = [
- {file = "flake8-bugbear-23.3.23.tar.gz", hash = "sha256:ea565bdb87b96b56dc499edd6cc3ba7f695373d902a5f56c989b74fad7c7719d"},
- {file = "flake8_bugbear-23.3.23-py3-none-any.whl", hash = "sha256:8a218d13abd6904800970381057ce8e72cde8eea743240c4ef3ede4dd0bc9cfb"},
-]
-
-[package.dependencies]
-attrs = ">=19.2.0"
-flake8 = ">=6.0.0"
-
-[package.extras]
-dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"]
-
-[[package]]
-name = "flake8-docstrings"
-version = "1.7.0"
-description = "Extension for flake8 which uses pydocstyle to check docstrings"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"},
- {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"},
-]
-
-[package.dependencies]
-flake8 = ">=3"
-pydocstyle = ">=2.1"
-
-[[package]]
-name = "flake8-isort"
-version = "6.0.0"
-description = "flake8 plugin that integrates isort ."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "flake8-isort-6.0.0.tar.gz", hash = "sha256:537f453a660d7e903f602ecfa36136b140de279df58d02eb1b6a0c84e83c528c"},
- {file = "flake8_isort-6.0.0-py3-none-any.whl", hash = "sha256:aa0cac02a62c7739e370ce6b9c31743edac904bae4b157274511fc8a19c75bbc"},
+ {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
+ {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
]
-[package.dependencies]
-flake8 = "*"
-isort = ">=5.0.0,<6"
-
[package.extras]
-test = ["pytest"]
-
-[[package]]
-name = "flake8-string-format"
-version = "0.3.0"
-description = "string format checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {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"},
-]
-
-[package.dependencies]
-flake8 = "*"
-
-[[package]]
-name = "flake8-tidy-imports"
-version = "4.8.0"
-description = "A flake8 plugin that helps you write tidier imports."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "flake8-tidy-imports-4.8.0.tar.gz", hash = "sha256:df44f9c841b5dfb3a7a1f0da8546b319d772c2a816a1afefcce43e167a593d83"},
- {file = "flake8_tidy_imports-4.8.0-py3-none-any.whl", hash = "sha256:25bd9799358edefa0e010ce2c587b093c3aba942e96aeaa99b6d0500ae1bf09c"},
-]
-
-[package.dependencies]
-flake8 = ">=3.8.0"
-
-[[package]]
-name = "flake8-todo"
-version = "0.7"
-description = "TODO notes checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"},
-]
-
-[package.dependencies]
-pycodestyle = ">=2.0.0,<3.0.0"
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]]
name = "frozenlist"
@@ -780,14 +636,14 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve
[[package]]
name = "identify"
-version = "2.5.18"
+version = "2.5.24"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"},
- {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"},
+ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
+ {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
]
[package.extras]
@@ -1002,18 +858,6 @@ htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=0.29.7)"]
[[package]]
-name = "mccabe"
-version = "0.7.0"
-description = "McCabe checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
- {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
-]
-
-[[package]]
name = "mslex"
version = "0.3.0"
description = "shlex for windows"
@@ -1125,21 +969,6 @@ files = [
setuptools = "*"
[[package]]
-name = "pep8-naming"
-version = "0.13.3"
-description = "Check PEP-8 naming conventions, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"},
- {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"},
-]
-
-[package.dependencies]
-flake8 = ">=5.0.0"
-
-[[package]]
name = "pillow"
version = "9.5.0"
description = "Python Imaging Library (Fork)"
@@ -1221,14 +1050,14 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]]
name = "pip-licenses"
-version = "4.2.0"
+version = "4.3.1"
description = "Dump the software license list of Python packages installed with pip."
category = "dev"
optional = false
python-versions = "~=3.8"
files = [
- {file = "pip-licenses-4.2.0.tar.gz", hash = "sha256:cabf1d83391c42278f1887a76555246bfcd9478c53be26e01e93b66d364f18a9"},
- {file = "pip_licenses-4.2.0-py3-none-any.whl", hash = "sha256:b6e057e359c0c4c927b7f70b43c27f06d9732a7830bb0aa14ac01f7281d3a972"},
+ {file = "pip-licenses-4.3.1.tar.gz", hash = "sha256:80236ae03036f0edccae57489de3932aa93984a7b552f4fd47399b08793918ff"},
+ {file = "pip_licenses-4.3.1-py3-none-any.whl", hash = "sha256:1e6652f7f3bd173e0a3551202e19518b21ac1d5ab1a2b54e08d96ee67146f9ca"},
]
[package.dependencies]
@@ -1239,30 +1068,30 @@ test = ["docutils", "mypy", "pytest-cov", "pytest-pycodestyle", "pytest-runner"]
[[package]]
name = "platformdirs"
-version = "3.1.0"
+version = "3.5.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
- {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
+ {file = "platformdirs-3.5.0-py3-none-any.whl", hash = "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4"},
+ {file = "platformdirs-3.5.0.tar.gz", hash = "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"},
]
[package.extras]
-docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "pre-commit"
-version = "3.2.2"
+version = "3.3.1"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"},
- {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"},
+ {file = "pre_commit-3.3.1-py2.py3-none-any.whl", hash = "sha256:218e9e3f7f7f3271ebc355a15598a4d3893ad9fc7b57fe446db75644543323b9"},
+ {file = "pre_commit-3.3.1.tar.gz", hash = "sha256:733f78c9a056cdd169baa6cd4272d51ecfda95346ef8a89bf93712706021b907"},
]
[package.dependencies]
@@ -1274,14 +1103,14 @@ virtualenv = ">=20.10.0"
[[package]]
name = "prettytable"
-version = "3.6.0"
+version = "3.7.0"
description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "prettytable-3.6.0-py3-none-any.whl", hash = "sha256:3b767129491767a3a5108e6f305cbaa650f8020a7db5dfe994a2df7ef7bad0fe"},
- {file = "prettytable-3.6.0.tar.gz", hash = "sha256:2e0026af955b4ea67b22122f310b90eae890738c08cb0458693a49b6221530ac"},
+ {file = "prettytable-3.7.0-py3-none-any.whl", hash = "sha256:f4aaf2ed6e6062a82fd2e6e5289bbbe705ec2788fe401a3a1f62a1cea55526d2"},
+ {file = "prettytable-3.7.0.tar.gz", hash = "sha256:ef8334ee40b7ec721651fc4d37ecc7bb2ef55fde5098d994438f0dfdaa385c0c"},
]
[package.dependencies]
@@ -1292,26 +1121,26 @@ tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"]
[[package]]
name = "psutil"
-version = "5.9.4"
+version = "5.9.5"
description = "Cross-platform lib for process and system monitoring in Python."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
- {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"},
- {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"},
- {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"},
- {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"},
- {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"},
- {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"},
- {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"},
- {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"},
- {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"},
- {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"},
- {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"},
- {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"},
- {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"},
- {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"},
+ {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"},
+ {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"},
+ {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"},
+ {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"},
+ {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"},
+ {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"},
+ {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"},
+ {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"},
+ {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"},
+ {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"},
+ {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"},
+ {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"},
+ {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"},
+ {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"},
]
[package.extras]
@@ -1386,18 +1215,6 @@ cffi = ">=1.5.0"
idna = ["idna (>=2.1)"]
[[package]]
-name = "pycodestyle"
-version = "2.10.0"
-description = "Python style guide checker"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
- {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
-]
-
-[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
@@ -1410,55 +1227,79 @@ files = [
]
[[package]]
-name = "pydis-core"
-version = "9.5.1"
-description = "PyDis core provides core functionality and utility to the bots of the Python Discord community."
+name = "pydantic"
+version = "1.10.7"
+description = "Data validation and settings management using python type hints"
category = "main"
optional = false
-python-versions = ">=3.10.0,<3.12.0"
+python-versions = ">=3.7"
files = [
- {file = "pydis_core-9.5.1-py3-none-any.whl", hash = "sha256:50bbf1800fe228dd60ba6624615815f45139c105512ef701c556ee7dedaa91eb"},
- {file = "pydis_core-9.5.1.tar.gz", hash = "sha256:83b89117def529c8b130f22c9f8cd46211df6329b039eedd5020098e656aa198"},
+ {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"},
+ {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"},
+ {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"},
+ {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"},
+ {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"},
+ {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"},
+ {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"},
+ {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"},
+ {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"},
+ {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"},
+ {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"},
+ {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"},
+ {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"},
+ {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"},
+ {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"},
+ {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"},
+ {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"},
+ {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"},
+ {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"},
+ {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"},
+ {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"},
+ {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"},
+ {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"},
+ {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"},
+ {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"},
+ {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"},
+ {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"},
+ {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"},
+ {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"},
+ {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"},
+ {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"},
+ {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"},
+ {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"},
+ {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"},
+ {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"},
+ {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"},
]
[package.dependencies]
-aiodns = "3.0.0"
-async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""}
-"discord.py" = "2.2.2"
-statsd = "4.0.1"
+python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""}
+typing-extensions = ">=4.2.0"
[package.extras]
-async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
[[package]]
-name = "pydocstyle"
-version = "6.3.0"
-description = "Python docstring style checker"
-category = "dev"
+name = "pydis-core"
+version = "9.6.0"
+description = "PyDis core provides core functionality and utility to the bots of the Python Discord community."
+category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.10.0,<3.12.0"
files = [
- {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"},
- {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"},
+ {file = "pydis_core-9.6.0-py3-none-any.whl", hash = "sha256:284b4587a124956220c186a3f774430cf1654d721c3c239640a204a53a567434"},
+ {file = "pydis_core-9.6.0.tar.gz", hash = "sha256:a88fcb2f2c2b16fa256228b684980b7f8aec7ee75a07e5868c69eb4a2162960b"},
]
[package.dependencies]
-snowballstemmer = ">=2.2.0"
+aiodns = "3.0.0"
+async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""}
+"discord.py" = "2.2.3"
+statsd = "4.0.1"
[package.extras]
-toml = ["tomli (>=1.2.3)"]
-
-[[package]]
-name = "pyflakes"
-version = "3.0.1"
-description = "passive checker of Python programs"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
- {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
-]
+async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"]
[[package]]
name = "pyjokes"
@@ -1507,7 +1348,7 @@ six = ">=1.5"
name = "python-dotenv"
version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1675,38 +1516,65 @@ full = ["numpy"]
[[package]]
name = "redis"
-version = "4.5.4"
+version = "4.5.5"
description = "Python client for Redis database and key-value store"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "redis-4.5.4-py3-none-any.whl", hash = "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2"},
- {file = "redis-4.5.4.tar.gz", hash = "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893"},
+ {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"},
+ {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"},
]
[package.dependencies]
-async-timeout = {version = ">=4.0.2", markers = "python_version <= \"3.11.2\""}
+async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""}
[package.extras]
hiredis = ["hiredis (>=1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
+name = "ruff"
+version = "0.0.267"
+description = "An extremely fast Python linter, written in Rust."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.0.267-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:4adbbbe314d8fcc539a245065bad89446a3cef2e0c9cf70bf7bb9ed6fe31856d"},
+ {file = "ruff-0.0.267-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:67254ae34c38cba109fdc52e4a70887de1f850fb3971e5eeef343db67305d1c1"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbe104f21a429b77eb5ac276bd5352fd8c0e1fbb580b4c772f77ee8c76825654"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db33deef2a5e1cf528ca51cc59dd764122a48a19a6c776283b223d147041153f"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9adf1307fa9d840d1acaa477eb04f9702032a483214c409fca9dc46f5f157fe3"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0afca3633c8e2b6c0a48ad0061180b641b3b404d68d7e6736aab301c8024c424"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2972241065b1c911bce3db808837ed10f4f6f8a8e15520a4242d291083605ab6"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f731d81cb939e757b0335b0090f18ca2e9ff8bcc8e6a1cf909245958949b6e11"},
+ {file = "ruff-0.0.267-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20c594eb56c19063ef5a57f89340e64c6550e169d6a29408a45130a8c3068adc"},
+ {file = "ruff-0.0.267-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:45d61a2b01bdf61581a2ee039503a08aa603dc74a6bbe6fb5d1ce3052f5370e5"},
+ {file = "ruff-0.0.267-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2107cec3699ca4d7bd41543dc1d475c97ae3a21ea9212238b5c2088fa8ee7722"},
+ {file = "ruff-0.0.267-py3-none-musllinux_1_2_i686.whl", hash = "sha256:786de30723c71fc46b80a173c3313fc0dbe73c96bd9da8dd1212cbc2f84cdfb2"},
+ {file = "ruff-0.0.267-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a898953949e37c109dd242cfcf9841e065319995ebb7cdfd213b446094a942f"},
+ {file = "ruff-0.0.267-py3-none-win32.whl", hash = "sha256:d12ab329474c46b96d962e2bdb92e3ad2144981fe41b89c7770f370646c0101f"},
+ {file = "ruff-0.0.267-py3-none-win_amd64.whl", hash = "sha256:d09aecc9f5845586ba90911d815f9772c5a6dcf2e34be58c6017ecb124534ac4"},
+ {file = "ruff-0.0.267-py3-none-win_arm64.whl", hash = "sha256:7df7eb5f8d791566ba97cc0b144981b9c080a5b861abaf4bb35a26c8a77b83e9"},
+ {file = "ruff-0.0.267.tar.gz", hash = "sha256:632cec7bbaf3c06fcf0a72a1dd029b7d8b7f424ba95a574aaa135f5d20a00af7"},
+]
+
+[[package]]
name = "sentry-sdk"
-version = "1.21.0"
+version = "1.23.0"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = false
python-versions = "*"
files = [
- {file = "sentry-sdk-1.21.0.tar.gz", hash = "sha256:36a1ca082a3065a8a05aafa4b1e3d74e9459d41fbb0bea8a5364caca68626341"},
- {file = "sentry_sdk-1.21.0-py2.py3-none-any.whl", hash = "sha256:82faf9e2c9eb77401a7a187094b126ca25c2a3a478de6704612f48b3346f7a84"},
+ {file = "sentry-sdk-1.23.0.tar.gz", hash = "sha256:58f4ff9e76c21bc7172eeec9f1bccb3ff2247c74c71d5590438ce36c803f46ea"},
+ {file = "sentry_sdk-1.23.0-py2.py3-none-any.whl", hash = "sha256:01b56a276642d31cf9b4aaf0b55938677265d7006be4785a10ef6330d0f5bba9"},
]
[package.dependencies]
certifi = "*"
-urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
+urllib3 = {version = ">=1.26.11,<2.0.0", markers = "python_version >= \"3.6\""}
[package.extras]
aiohttp = ["aiohttp (>=3.5)"]
@@ -1718,10 +1586,11 @@ chalice = ["chalice (>=1.16.0)"]
django = ["django (>=1.8)"]
falcon = ["falcon (>=1.4)"]
fastapi = ["fastapi (>=0.79.0)"]
-flask = ["blinker (>=1.1)", "flask (>=0.11)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
grpcio = ["grpcio (>=1.21.1)"]
httpx = ["httpx (>=0.16.0)"]
huey = ["huey (>=2)"]
+loguru = ["loguru (>=0.5)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
pure-eval = ["asttokens", "executing", "pure-eval"]
pymongo = ["pymongo (>=3.1)"]
@@ -1736,14 +1605,14 @@ tornado = ["tornado (>=5)"]
[[package]]
name = "setuptools"
-version = "67.4.0"
+version = "67.7.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"},
- {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"},
+ {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"},
+ {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"},
]
[package.extras]
@@ -1764,18 +1633,6 @@ files = [
]
[[package]]
-name = "snowballstemmer"
-version = "2.2.0"
-description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
- {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
-]
-
-[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
@@ -1789,14 +1646,14 @@ files = [
[[package]]
name = "soupsieve"
-version = "2.4"
+version = "2.4.1"
description = "A modern CSS selector implementation for Beautiful Soup."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"},
- {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"},
+ {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"},
+ {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"},
]
[[package]]
@@ -1842,15 +1699,27 @@ files = [
]
[[package]]
+name = "typing-extensions"
+version = "4.5.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
+ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+]
+
+[[package]]
name = "urllib3"
-version = "1.26.14"
+version = "1.26.15"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
files = [
- {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"},
- {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
+ {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
+ {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
]
[package.extras]
@@ -1860,24 +1729,24 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
-version = "20.20.0"
+version = "20.23.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"},
- {file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"},
+ {file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"},
+ {file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"},
]
[package.dependencies]
distlib = ">=0.3.6,<1"
-filelock = ">=3.4.1,<4"
-platformdirs = ">=2.4,<4"
+filelock = ">=3.11,<4"
+platformdirs = ">=3.2,<4"
[package.extras]
-docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
-test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"]
[[package]]
name = "wcwidth"
@@ -1893,86 +1762,86 @@ files = [
[[package]]
name = "yarl"
-version = "1.8.2"
+version = "1.9.2"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"},
- {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"},
- {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"},
- {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"},
- {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"},
- {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"},
- {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"},
- {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"},
- {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"},
- {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"},
- {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"},
- {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"},
- {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"},
- {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"},
- {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"},
- {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"},
- {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"},
- {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"},
- {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"},
- {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"},
- {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"},
- {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"},
- {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"},
- {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"},
- {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"},
- {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"},
- {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"},
- {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"},
- {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"},
- {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"},
- {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"},
- {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"},
- {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"},
- {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"},
- {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"},
- {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"},
- {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"},
- {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"},
- {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"},
- {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"},
- {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"},
- {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"},
- {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"},
- {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"},
- {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"},
- {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"},
- {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"},
- {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"},
- {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"},
- {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"},
- {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"},
- {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"},
- {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"},
- {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"},
- {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"},
- {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"},
- {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"},
- {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"},
- {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"},
- {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"},
- {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"},
- {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"},
- {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"},
- {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"},
- {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"},
- {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"},
- {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"},
- {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"},
- {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"},
- {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"},
- {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"},
- {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"},
- {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"},
- {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"},
+ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"},
+ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"},
+ {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"},
+ {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"},
+ {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"},
+ {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"},
+ {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"},
+ {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"},
+ {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"},
+ {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"},
+ {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"},
+ {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"},
+ {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"},
+ {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"},
+ {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"},
+ {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"},
+ {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"},
+ {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"},
+ {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"},
+ {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"},
+ {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"},
+ {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"},
+ {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
+ {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
]
[package.dependencies]
@@ -1982,4 +1851,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "3.11.*"
-content-hash = "a67fc2b1b3a3a2bdd858c160ffcc1a1b74532f2a1a4ab4659b62687d43934d7d"
+content-hash = "dca820268faec5488f1769c5acbeb43593514061c835f5b648866ac4072a2c9e"
diff --git a/pyproject.toml b/pyproject.toml
index e99c87a4..ae3b3245 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,14 +9,14 @@ license = "MIT"
python = "3.11.*"
# See https://bot-core.pythondiscord.com/ for docs.
-pydis_core = { version = "9.5.1", extras = ["async-rediscache"] }
+pydis_core = { version = "9.6.0", extras = ["async-rediscache"] }
aiodns = "3.0.0"
rapidfuzz = "3.0.0"
arrow = "1.2.3"
beautifulsoup4 = "4.12.2"
pillow = "9.5.0"
-sentry-sdk = "1.21.0"
+sentry-sdk = "1.23.0"
PyYAML = "6.0"
emojis = "0.7.0"
coloredlogs = "15.0.1"
@@ -24,20 +24,15 @@ colorama = { version = "0.4.6", markers = "sys_platform == 'win32'" }
lxml = "4.9.2"
emoji = "2.2.0"
pyjokes = "0.6.0"
+pydantic = { version = "1.10.7", extras = ["dotenv"]}
+
[tool.poetry.dev-dependencies]
-flake8 = "6.0.0"
-flake8-annotations = "3.0.0"
-flake8-bugbear = "23.3.23"
-flake8-docstrings = "1.7.0"
-flake8-string-format = "0.3.0"
-flake8-tidy-imports = "4.8.0"
-flake8-todo = "0.7"
-flake8-isort = "6.0.0"
-pep8-naming = "0.13.3"
-pip-licenses = "4.2.0"
-pre-commit = "3.2.2"
+isort = "5.12.0"
+pip-licenses = "4.3.1"
+pre-commit = "3.3.1"
python-dotenv = "1.0.0"
+ruff = "0.0.267"
taskipy = "1.10.4"
[tool.taskipy.tasks]
@@ -58,3 +53,21 @@ combine_as_imports = true
line_length = 120
atomic = true
known_first_party = ["bot"]
+
+[tool.ruff]
+target-version = "py311"
+extend-exclude = [".cache"]
+ignore = [
+ "ANN002", "ANN003", "ANN101", "ANN102", "ANN204", "ANN206", "ANN401",
+ "B904",
+ "C401", "C408",
+ "D100", "D104", "D105", "D107", "D203", "D212", "D214", "D215", "D301",
+ "D400", "D401", "D402", "D404", "D405", "D406", "D407", "D408", "D409", "D410", "D411", "D412", "D413", "D414", "D416", "D417",
+ "E731",
+ "RET504",
+ "RUF005",
+ "S311",
+ "SIM102", "SIM108",
+]
+line-length = 120
+select = ["ANN", "B", "C4", "D", "DTZ", "E", "F", "ISC", "INT", "N", "PGH", "PIE", "Q", "RET", "RSE", "RUF", "S", "SIM", "T20", "TID", "UP", "W"]
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 7906e6d9..00000000
--- a/tox.ini
+++ /dev/null
@@ -1,26 +0,0 @@
-[flake8]
-max-line-length=120
-application_import_names=bot
-docstring-convention=all
-extend-ignore=
- P102,B311,W503,E226,S311,
- # Missing Docstrings
- D100,D104,D105,D107,
- # Docstring Whitespace
- D203,D212,D214,D215,
- # Docstring Quotes
- D301,D302,
- # Docstring Content
- D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417,
- # Type Annotations
- ANN002,ANN003,ANN101,ANN102,ANN204,ANN206,
- # Binary operators over multiple lines
- W504,
-exclude=
- __pycache__,.cache,
- venv,.venv,
- tests,
-per-file-ignores =
- # Don't require docstrings in constants
- constants.py:D101
-import-order-style=pycharm