diff --git a/ornate-orbits/.github/workflows/lint.yaml b/ornate-orbits/.github/workflows/lint.yaml new file mode 100644 index 0000000..7f67e80 --- /dev/null +++ b/ornate-orbits/.github/workflows/lint.yaml @@ -0,0 +1,35 @@ +# GitHub Action workflow enforcing our code style. + +name: Lint + +# Trigger the workflow on both push (to the main repository, on the main branch) +# and pull requests (against the main repository, but from any repo, from any branch). +on: + push: + branches: + - main + pull_request: + +# Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit. +# It is useful for pull requests coming from the main repository since both triggers will match. +concurrency: lint-${{ github.sha }} + +jobs: + lint: + runs-on: ubuntu-latest + + env: + # The Python version your project uses. Feel free to change this if required. + PYTHON_VERSION: "3.12" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 diff --git a/ornate-orbits/.gitignore b/ornate-orbits/.gitignore new file mode 100644 index 0000000..233eb87 --- /dev/null +++ b/ornate-orbits/.gitignore @@ -0,0 +1,31 @@ +# Files generated by the interpreter +__pycache__/ +*.py[cod] + +# Environment specific +.venv +venv +.env +env + +# Unittest reports +.coverage* + +# Logs +*.log + +# PyEnv version selector +.python-version + +# Built objects +*.so +dist/ +build/ + +# IDEs +# PyCharm +.idea/ +# VSCode +.vscode/ +# MacOS +.DS_Store diff --git a/ornate-orbits/.pre-commit-config.yaml b/ornate-orbits/.pre-commit-config.yaml new file mode 100644 index 0000000..4bccb6f --- /dev/null +++ b/ornate-orbits/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# Pre-commit configuration. +# See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.0 + hooks: + - id: ruff + - id: ruff-format diff --git a/ornate-orbits/LICENSE.txt b/ornate-orbits/LICENSE.txt new file mode 100644 index 0000000..5a04926 --- /dev/null +++ b/ornate-orbits/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2021 Python Discord + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ornate-orbits/Makefile b/ornate-orbits/Makefile new file mode 100644 index 0000000..273786d --- /dev/null +++ b/ornate-orbits/Makefile @@ -0,0 +1,13 @@ +.PHONY: tc +tc: + @python -m mypy . + + +.PHONY: test +test: + @python -m pytest . + + +.PHONY: testv +testv: + @python -m pytest -s . diff --git a/ornate-orbits/README.md b/ornate-orbits/README.md new file mode 100644 index 0000000..b1d5f72 --- /dev/null +++ b/ornate-orbits/README.md @@ -0,0 +1,71 @@ +# Mordle +Wordle with more fun and more information. +See more in [this slide](https://docs.google.com/presentation/d/1cCAn2Ggavuf-VrAuh-mOnEE_v00eoi68kCEKvENDXug/edit?usp=sharing) to acknowledge all of its functionalities. + +## Installation +Mordle requires only several dependencies to run, be sure to clone it first: +``` +git clone git@github.com:cin-lawrence/code-jam-2024 +``` +Ensure you have a Bot account and a server to run the Bot. + +To create a Bot account, see more [here](https://discordpy.readthedocs.io/en/stable/discord.html#creating-a-bot-account). + +Upon creating a Bot account and a Discord server, retrieve the Bot token and the server ID (see `Server Settings > Widget > Server ID`, then copy it). + +**The application can be installed using either of 2 ways below.** + +### In the host machine +Change your working directory to the project's root folder. +``` +cd code-jam-2024 +``` +Install the dependencies. _It could be nicer if you have a virtual environment._ +``` +pip install -r requirements-dev.txt +``` +Exports your environment variables, including the Bot token and the server ID retrieved. +``` +export DISCORD_TOKEN= +export GUILD_ID= +``` +Run the application from the current working directory. +``` +python -m app.main +``` + +### Using Docker +Docker encapsulates all requirements needed for the application. + +Though more advanced, this is the recommended way to run the app. + +Follow the [official documentation](https://docs.docker.com/get-docker/) to install Docker. + +Rename the `.env` in `./config` folder, +``` +mv ./config/app/.env.example ./config/app/.env +``` +and add your environment variables. +``` +DISCORD_TOKEN= +GUILD_ID= +``` +The latest version of Docker already has docker-compose included. To minimize the steps needed, it is recommended to use docker-compose. +``` +docker compose build +docker compose up -d +``` +The steps above are sufficient to spin up the application. To shut down the application: +``` +docker compose down +``` +If you want to use the pre-built image: +``` +docker run --env-file ./config/app/.env mikosurge/mordle:v0.0.1 +``` + +## The Ornate Orbits team +- **@Atonement**: repository setup, first bot implementation, code refactoring, trivia crawling, commits, and PRs managing. +- **@Xerif**: main game logic, most of the commands, slideshow creator. +- **@kvothe**: some nltk, crawling, math, and trivia logic. +- **@Bh**: tests writing, bug fixing. diff --git a/ornate-orbits/app/__init__.py b/ornate-orbits/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ornate-orbits/app/bot.py b/ornate-orbits/app/bot.py new file mode 100644 index 0000000..2794319 --- /dev/null +++ b/ornate-orbits/app/bot.py @@ -0,0 +1,221 @@ +import logging +from typing import cast +from uuid import UUID + +from discord import Client, Intents, Object, TextChannel +from discord.ext import commands +from discord.interactions import Interaction + +from .core import ui +from .core.wordle import UnequalInLengthError, WordleGame +from .models.wordle import Wordle, WordleStatus +from .settings import BotSettings, settings +from .storage.guess import guess_repo +from .storage.player import player_repo +from .storage.trivia import trivia_repo +from .storage.wordle import wordle_repo + +logger = logging.getLogger(__name__) + + +class Bot(commands.Bot): + """Overriden class for the default discord Bot.""" + + def __init__(self, settings: BotSettings) -> None: + self.settings = settings + super().__init__(settings.COMMAND_PREFIX, intents=Intents.default()) + + async def on_ready(self) -> None: + """Overriden method on_ready.""" + logger.warning( + "[bot] syncing commands into server %s", + self.settings.GUILD_ID, + ) + await bot.tree.sync(guild=Object(id=settings.GUILD_ID)) + logger.warning("DONE syncing commands!") + + +bot = Bot(settings) + + +@bot.tree.command( + name="start-wordle", + description="Start the wordle game", + guild=Object(id=settings.GUILD_ID), +) +async def start_wordle(interaction: Interaction[Client]) -> None: + """Start the wordle game.""" + if await wordle_repo.get_active_wordle_by_user_id(interaction.user.id): + await interaction.response.send_message( + "You already starts the wordle game\n\ + Please complete the current game to start a new game", + ) + return + + player = await player_repo.get(interaction.user.id) + if player is None: + await player_repo.create( + userid=interaction.user.id, + username=interaction.user.name, + display_name=interaction.user.display_name, + ) + await interaction.response.defer() + + view_menu = ui.StartSelectionView() + + await interaction.followup.send("Welcome to wordle", view=view_menu) + + +@bot.tree.command( + name="guess", + description="make a guess on the wordle", + guild=Object(id=settings.GUILD_ID), +) +async def guess(interaction: Interaction[Client], word: str) -> None: + """User guess the wordle.""" + wordle_game = WordleGame() + + if not wordle_game.check_valid_word(word=word.upper()): + await interaction.response.send_message( + f"{word.upper()} is not a valid word.", + ) + return + + wordle: Wordle | None = await wordle_repo.get_ongoing_wordle( + interaction.user.id, + ) + if wordle is None: + message = "Please start the wordle game before making a guess." + await interaction.response.send_message(message) + return + + match wordle.status: + case WordleStatus.ACTIVE.value: + try: + await wordle_game.guess( + user_id=interaction.user.id, + guess=word.upper(), + ) + except UnequalInLengthError: + message = "The length of guess and the word are not the same" + await interaction.response.send_message(content=message) + return + else: + embed = ui.GuessEmbed( + user=interaction.user, + guesses=await wordle_repo.get_guesses( + user_id=interaction.user.id + ), + ) + + await interaction.response.send_message(embed=embed) + case WordleStatus.PENDING.value: + message = ( + "Please complete the trivia question first " + "before continue guessing" + ) + await interaction.response.send_message(message) + return + case _: + return + + if not (await wordle_game.check_guess(interaction.user.id)): + await wordle_game.wrong_guess(id=wordle.id) + wordle = await wordle_repo.get_pending_wordle(interaction.user.id) + + if wordle: + await trivial(interaction=interaction, wordle_id=wordle.id) + return + else: + results = await wordle_repo.get_guesses(interaction.user.id) + await cast(TextChannel, interaction.channel).send( + content=f"Congratulations! {interaction.user.name}" + f"has guess the correct word in {len(results)} guess(es)" + ) + await wordle_game.win(interaction.user.id) + + +@bot.tree.command( + name="end-wordle", + description="end the current wordle game", + guild=Object(id=settings.GUILD_ID), +) +async def end_wordle(interaction: Interaction[Client]) -> None: + """User end the current wordle game.""" + if await wordle_repo.get_active_wordle_by_user_id( + user_id=interaction.user.id, + ) or await wordle_repo.get_pending_wordle(user_id=interaction.user.id): + await interaction.response.send_message("The current game ends") + + await WordleGame().end(interaction.user.id) + + else: + await interaction.response.send_message("You are not in a game yet.") + + +async def trivial(interaction: Interaction[Client], wordle_id: UUID) -> None: + """Show the trivial question.""" + trivia_ques = await trivia_repo.get_random() + + view = ui.TrivialSelectionView( + correct_answer=trivia_ques.correct_answer, + wrong_answers=[ + trivia_ques.incorrect_answer_1, + trivia_ques.incorrect_answer_2, + trivia_ques.incorrect_answer_3, + ], + wordle_id=wordle_id, + ) + + if interaction.response.is_done(): + await interaction.followup.send( + content=trivia_ques.question, view=view + ) + else: + await interaction.response.send_message( + content=trivia_ques.question, view=view + ) + + +@bot.tree.command( + name="player-stats", + description="Show the player stats", + guild=Object(id=settings.GUILD_ID), +) +async def show_player_stats(interaction: Interaction[Client]) -> None: + """Show the player stats.""" + wordles = await wordle_repo.get_by_user_id(interaction.user.id) + wins = [ + wordle + for wordle in wordles + if wordle.status == WordleStatus.COMPLETED.value + ] + num_guesses = await guess_repo.count_by_wordle_ids( + [wordle.id for wordle in wordles] + ) + player = await player_repo.get(interaction.user.id) + player_id = player.id if player else interaction.user.id + player_name = player.display_name if player else "unknown" + await interaction.response.send_message( + embed=ui.PlayerStatEmbed( + player_id, + player_name, + len(wordles), + len(wins), + num_guesses, + ) + ) + + +@bot.tree.command( + name="help", + description="See all the commands.", + guild=Object(id=settings.GUILD_ID), +) +async def help(interaction: Interaction[Client]) -> None: + """Show all the commands.""" + commands = bot.tree.get_commands(guild=Object(id=settings.GUILD_ID)) + + embed = ui.HelpEmbed(commands=commands) + + await interaction.response.send_message(embed=embed) diff --git a/ornate-orbits/app/container.py b/ornate-orbits/app/container.py new file mode 100644 index 0000000..e69de29 diff --git a/ornate-orbits/app/core/__init__.py b/ornate-orbits/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ornate-orbits/app/core/ui.py b/ornate-orbits/app/core/ui.py new file mode 100644 index 0000000..83b3e3e --- /dev/null +++ b/ornate-orbits/app/core/ui.py @@ -0,0 +1,297 @@ +from collections.abc import Sequence +from random import shuffle +from typing import Final +from uuid import UUID + +from discord import ( + Client, + Embed, + Interaction, + Member, + SelectOption, + User, +) +from discord.app_commands import Command +from discord.ui import Select, View + +from app.core.wordle import WordleGame +from app.models.guess import Guess +from app.storage.wordle import wordle_repo +from app.word_generator import Difficulty + +EMOJI: Final[list[str]] = [ + ":green_heart:", + ":yellow_heart:", + ":blue_heart:", + ":purple_heart:", + ":heart:", +] + + +class GuessEmbed(Embed): + """Embed that show the guesses of the player.""" + + _2_SPACES: Final[str] = " " + _4_SPACES: Final[str] = " " + + def __init__(self, user: User | Member, guesses: Sequence[Guess]) -> None: + super().__init__(title=f"{user.name}'s Wordle Guess") + self.color = 0xFF0000 if int(guesses[-1].result) != 0 else 0x00FF00 + + for idx, guess in enumerate(guesses): + self.add_field( + name=f"Guess #{idx + 1}", + value=( + f"{self._format_guess_word(guess.content)}\n{self._format_guess_result(guess.result)}" + ), + inline=False, + ) + + def _format_guess_word(self, word: str) -> str: + """Format the guess word to show on the embed.""" + new_word = f"{self._2_SPACES}{self._4_SPACES.join(word)}" + + return ( + new_word.replace("L", " L ") + .replace("J", " J ") + .replace("I", " I ") + ) + + def _format_guess_result(self, word: str) -> str: + """Format the result into emoji to show on the embed.""" + return " ".join(EMOJI[int(val)] for val in word) + + +class PlayerStatEmbed(Embed): + """Embed that show the player stats.""" + + def __init__( # noqa: PLR0913 + self, + player_id: int, + player_name: str, + num_wordle_games: int, + num_wins: int, + num_guesses: int, + ) -> None: + super().__init__(title=f"{player_name}'s stats") + + self.add_field( + name="Player ID", + value=player_id, + inline=False, + ) + + self.add_field( + name="Player name", + value=player_name, + inline=False, + ) + + self.add_field( + name="Wordle Games", + value=num_wordle_games, + inline=False, + ) + + self.add_field( + name="Wins", + value=num_wins, + inline=False, + ) + + self.add_field( + name="Guesses", + value=num_guesses, + inline=False, + ) + + self.add_field( + name="Average guesses per game", + value=num_guesses // num_wordle_games, + inline=False, + ) + + +class HelpEmbed(Embed): + """Embed for the help comment.""" + + def __init__(self, commands: list[Command]) -> None: + super().__init__(title="Command Info") + + for command in commands: + self.add_field( + name=f"/{command.name}", + value=f" :right_arrow: {command.description}", + inline=False, + ) + + +class StartSelectionView(View): + """View that contains all the Select when game start.""" + + def __init__(self, *, timeout: float | None = 180) -> None: + self.length_selected = False + self.difficulty_selected = False + super().__init__(timeout=timeout) + + self.length_select = LengthSelect() + self.difficulty_select = DifficultySelect() + + self.add_item(self.length_select) + self.add_item(self.difficulty_select) + + async def start(self, interaction: Interaction[Client]) -> None: + """Start the Wordle Game.""" + await WordleGame().start( + interaction=interaction, + length_select=self.length_select, + difficulty_select=self.difficulty_select, + ) + + +class LengthSelect(Select[StartSelectionView]): + """Select that choose the length of a word in a Wordle Game.""" + + OPTION_PLACEHOLDER: Final[str] = "Choose Length of Word" + MIN_VALUES: Final[int] = 1 + MAX_VALUES: Final[int] = 1 + + def __init__(self) -> None: + options = [ + SelectOption( + label=f"{val} letters", + value=str(val), + description=f"The Wordle Game will be a {val} letters word.", + ) + for val in range(5, 16) + ] + + super().__init__( + placeholder=self.OPTION_PLACEHOLDER, + min_values=self.MIN_VALUES, + max_values=self.MAX_VALUES, + options=options, + ) + + self.add_option( + label="Random", + value="0", + description="Choose a random letter word for the Wordle Game.", + ) + + async def callback(self, interaction: Interaction[Client]) -> None: + """User made a selection.""" + if self.view is None: + return + + self.view.length_selected = True + + if self.view.length_selected and self.view.difficulty_selected: + await self.view.start(interaction) + else: + await interaction.response.defer() + + +class DifficultySelect(Select[StartSelectionView]): + """Select that choose the length of a word in a Wordle Game.""" + + OPTION_PLACEHOLDER: Final[str] = "Choose Wordle Game Difficulty" + MIN_VALUES: Final[int] = 1 + MAX_VALUES: Final[int] = 1 + + def __init__(self) -> None: + options = [ + SelectOption( + label=val, + value=val, + description=f"{val} Mode of Wordle", + # emoji + ) + for val in Difficulty + ] + + super().__init__( + placeholder=self.OPTION_PLACEHOLDER, + min_values=self.MIN_VALUES, + max_values=self.MAX_VALUES, + options=options, + ) + + async def callback(self, interaction: Interaction[Client]) -> None: + """User made a selection.""" + if self.view is None: + return + + self.view.difficulty_selected = True + + if self.view.length_selected and self.view.difficulty_selected: + await self.view.start(interaction) + else: + await interaction.response.defer() + + +class TrivialSelectionView(View): + """View that contains all the Select in a trivial question.""" + + OPTION_PLACEHOLDER: Final[str] = "Select the correct answer" + MIN_VALUES: Final[int] = 1 + MAX_VALUES: Final[int] = 1 + + CORRECT_VALUE: Final[str] = "0" + + def __init__( + self, + correct_answer: str, + wrong_answers: list[str], + wordle_id: UUID, + *, + timeout: float | None = 180, + ) -> None: + self.wordle_id = wordle_id + super().__init__(timeout=timeout) + + options = [ + SelectOption( + label=ans, + value=str(idx), + ) + for idx, ans in enumerate(wrong_answers, 1) + ] + + options.append(SelectOption(label=correct_answer, value="0")) + + shuffle(options) + + self.select: Select[TrivialSelectionView] = Select( + placeholder=self.OPTION_PLACEHOLDER, + min_values=self.MIN_VALUES, + max_values=self.MAX_VALUES, + options=options, + ) + + self.select.callback = self.check_answer # type: ignore[method-assign] + + self.add_item(self.select) + + async def check_answer(self, interaction: Interaction[Client]) -> None: + """Check if the selection is same as the answer.""" + if self.select.values[0] != self.CORRECT_VALUE: # noqa: PD011 + await interaction.response.send_message("Wrong Answer") + await wordle_repo.change_status( + id=self.wordle_id, is_winning=False + ) + return + + await interaction.response.send_message("Correct Answer") + await wordle_repo.change_status(id=self.wordle_id, is_winning=False) + + wordle_game = WordleGame() + wordle = await wordle_repo.get_ongoing_wordle( + user_id=interaction.user.id + ) + await interaction.followup.send("Incoming hint ...") + + hint = await wordle_game.get_hint( + user_id=interaction.user.id, word=wordle.word + ) + await interaction.followup.send(hint) diff --git a/ornate-orbits/app/core/wordle.py b/ornate-orbits/app/core/wordle.py new file mode 100644 index 0000000..ea6dbcf --- /dev/null +++ b/ornate-orbits/app/core/wordle.py @@ -0,0 +1,193 @@ +import logging +import secrets +from collections.abc import Generator +from typing import Any, Final +from uuid import UUID + +from discord import Client +from discord.interactions import Interaction +from discord.ui import Select, View + +from app.enums import MatchResult +from app.storage.guess import guess_repo +from app.storage.wordle import wordle_repo +from app.word_generator import Difficulty, Word, WordGenerator, get_wordgen + +logger = logging.getLogger(__name__) + + +class UnequalInLengthError(Exception): + """Guess and word are unequal in length.""" + + +class WordleGameNotFoundError(Exception): + """Exception raised when the wordle game not found.""" + + +class WordleGame: + """Represent a Wordle Game.""" + + WORD_LENGTH_MIN: Final[int] = 5 + WORD_LENGTH_MAX: Final[int] = 15 + DEVIATED_THRESHOLD: Final[int] = 4 + + def __init__(self) -> None: + self.wordgen: WordGenerator = get_wordgen() + + def _random_length(self) -> int: + return self.WORD_LENGTH_MIN + secrets.randbelow( + self.WORD_LENGTH_MAX - self.WORD_LENGTH_MIN + 1, + ) + + def _gen_word(self, length: int | None, difficulty: Difficulty) -> str: + """Generate a new word.""" + length = length or self._random_length() + return self.wordgen.random( + length=length, difficulty=difficulty + ).word.upper() + + def _gen_color( + self, + guesschar: str, + wordchar: str, + word: str, + ) -> int: + """Generate color for each char. + + - ❤️ for wrong letter (4) + - 💛 for correct letter, wrong position (1) + - 💚 for correct letter, correct location (0) + - 💙 for deviated letter, correct position (2) + - 💜 for deviated letter, wrong position (3) + """ + if guesschar == wordchar: + return MatchResult.CORRECT_LETTER_CORRECT_POSITION + if guesschar in word: + return MatchResult.CORRECT_LETTER_WRONG_POSITION + guess_ascii: int = ord(guesschar) + word_ascii: int = ord(wordchar) + if abs(guess_ascii - word_ascii) < self.DEVIATED_THRESHOLD: + return MatchResult.DEVIATED_LETTER_CORRECT_POSITION + if any( + abs(guess_ascii - ord(ch)) < self.DEVIATED_THRESHOLD for ch in word + ): + return MatchResult.DEVIATED_LETTER_WRONG_POSITION + return MatchResult.WRONG_LETTER + + def gen_colors_for_guess( + self, + guess: str, + word: str, + ) -> Generator[int, Any, Any]: + """Generate the guess result in integers.""" + for guesschar, wordchar in zip(guess, word, strict=False): + yield self._gen_color(guesschar, wordchar, word) + + async def start( + self, + interaction: Interaction[Client], + length_select: Select[View], + difficulty_select: Select[View], + ) -> str: + """Start the game.""" + word = self._gen_word( + length=int(length_select.values[0]), # noqa:PD011 + difficulty=Difficulty(difficulty_select.values[0]), # noqa:PD011 + ) + message = "You word is chosen." + message += ( + f"The word has {len(word)} letters." + if length_select.values[0] == "0" # noqa:PD011 + else "" + ) + message += "You can start guessing the word now." + + await wordle_repo.create(word, interaction.user.id) + await interaction.response.send_message(content=message) + return word + + async def guess( + self, + user_id: int, + guess: str, + ) -> None: + """Save the guess result into the DB.""" + wordle = await wordle_repo.get_active_wordle_by_user_id( + user_id=user_id, + ) + if wordle is None: + raise ValueError("wordle game not found for user %d" % user_id) + + if len(guess) != len(wordle.word): + raise UnequalInLengthError + + colors = self.gen_colors_for_guess(guess=guess, word=wordle.word) + await guess_repo.create( + content=guess, + result="".join(map(str, colors)), + wordle_id=wordle.id, + ) + + async def end(self, user_id: int) -> None: + """End the current wordle game of a user.""" + wordle = await wordle_repo.get_ongoing_wordle( + user_id=user_id, + ) + if not wordle: + raise WordleGameNotFoundError + await wordle_repo.change_status( + id=wordle.id, + is_winning=False, + is_ending=True, + ) + + async def win(self, user_id: int) -> None: + """Win the current wordle game of a user.""" + wordle = await wordle_repo.get_ongoing_wordle( + user_id=user_id, + ) + if not wordle: + raise WordleGameNotFoundError + await wordle_repo.change_status( + id=wordle.id, + is_winning=True, + is_ending=False, + ) + + async def check_guess(self, user_id: int) -> bool: + """Return True if the guess match the active wordle.""" + guesses = await wordle_repo.get_guesses(user_id=user_id) + latest_guess = guesses[-1].result + + return not any(map(int, latest_guess)) + + def check_valid_word(self, word: str) -> bool: + """Return True if the word is valid.""" + return self.wordgen.is_valid(word.lower()) + + async def wrong_guess(self, id: UUID) -> None: + """The previous guess is wrong.""" + await wordle_repo.change_status(id=id, is_winning=False) + + async def get_hint(self, user_id: int, word: str) -> str: + """Return hint for the user.""" + target_word: Word = self.wordgen.get_word(word=word) + + choice = secrets.randbelow(10) + + if choice < 8: # noqa: PLR2004 + return await self.get_letter_hint(user_id=user_id) + if choice == 8: # noqa: PLR2004 + return f"The definition of the word is {target_word.definition}" + + return f"The synonyms of the word are {"," + .join(target_word.synonyms)}" + + async def get_letter_hint(self, user_id: int) -> str: + """Return the correct letter at specific position.""" + wordle_game = await wordle_repo.get_ongoing_wordle(user_id=user_id) + position = secrets.randbelow(len(wordle_game.word)) + + return ( + f"{wordle_game.word[position]} is at the position {position + 1}" + ) diff --git a/ornate-orbits/app/entrypoint.sh b/ornate-orbits/app/entrypoint.sh new file mode 100644 index 0000000..2a786d9 --- /dev/null +++ b/ornate-orbits/app/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash + + +watchmedo auto-restart --directory=./app/ --pattern=*.py --recursive --debounce-interval=2 -- python -m app.main diff --git a/ornate-orbits/app/enums.py b/ornate-orbits/app/enums.py new file mode 100644 index 0000000..e559b84 --- /dev/null +++ b/ornate-orbits/app/enums.py @@ -0,0 +1,11 @@ +from enum import IntEnum + + +class MatchResult(IntEnum): + """Meaningful guess result.""" + + CORRECT_LETTER_CORRECT_POSITION = 0 + CORRECT_LETTER_WRONG_POSITION = 1 + DEVIATED_LETTER_CORRECT_POSITION = 2 + DEVIATED_LETTER_WRONG_POSITION = 3 + WRONG_LETTER = 4 diff --git a/ornate-orbits/app/gather_trivias.py b/ornate-orbits/app/gather_trivias.py new file mode 100644 index 0000000..db97751 --- /dev/null +++ b/ornate-orbits/app/gather_trivias.py @@ -0,0 +1,117 @@ +# ref: https://github.com/blobfysh/opentdb-api/blob/master/index.js +import asyncio +import logging +import os +from typing import Final, TypedDict + +import httpx + +from .models.base import Base +from .models.trivia import Trivia, TriviaDifficulty +from .storage.database import trivia_database +from .storage.trivia import trivia_repo + +OPENTDB_ENDPOINT: Final[str] = ( + "https://opentdb.com/api.php?amount=50&type=multiple&token={token}" +) +HTTP_200_OK: Final[int] = 200 +logger = logging.getLogger("gather_trivia") + + +class OpentdbResult(TypedDict): + """Model for result in opentdb.""" + + type: str + difficulty: str + category: str + question: str + correct_answer: str + incorrect_answers: list[str] + + +class OpentdbResponse(TypedDict): + """Model for opentdb response.""" + + response_code: int + results: list[OpentdbResult] + + +def esc(text: str) -> str: + """De-escape all escaped strings.""" + return ( + text.replace(""", '"') + .replace("'", "'") + .replace("&", "&") + .replace("´", "`") + .replace("é", "é") + .replace("ó", "ó") + .replace("£", "£") + .replace("á", "á") + .replace("Á", "Á") + .replace("ñ", "ñ") + .replace("”", '"') + .replace("ö", "ö") + ) + + +async def main() -> None: + """The main gather function.""" + token: str = os.environ["TOKEN"] + response_code: int = 0 + + while response_code == 0: + try: + async with httpx.AsyncClient() as client: + response = await client.post( + url=OPENTDB_ENDPOINT.format(token=token), + timeout=None, + ) + if response.status_code != HTTP_200_OK: + logger.warning( + "abnormal status_code %d", response.status_code + ) + continue + response_dict: OpentdbResponse = response.json() + response_code = response_dict.get("response_code", 0) + for result in response_dict.get("results", []): + logger.info(result) + await trivia_repo.create( + Trivia( + difficulty=TriviaDifficulty(result["difficulty"]), + category=esc(result["category"]), + question=esc(result["question"]), + correct_answer=esc(result["correct_answer"]), + incorrect_answer_1=esc( + result["incorrect_answers"][0] + ), + incorrect_answer_2=esc( + result["incorrect_answers"][1] + ), + incorrect_answer_3=esc( + result["incorrect_answers"][2] + ), + ) + ) + except Exception as exc: + logger.exception(exc) + continue + finally: + await asyncio.sleep(2) + + +async def init_db() -> None: + """Initialize database.""" + async with trivia_database.engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_random() -> None: + """Return a random trivia.""" + logger.info((await trivia_repo.get_random()).as_dict()) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + asyncio.run(init_db()) + asyncio.run(main()) + asyncio.run(get_random()) diff --git a/ornate-orbits/app/main.py b/ornate-orbits/app/main.py new file mode 100644 index 0000000..66b794f --- /dev/null +++ b/ornate-orbits/app/main.py @@ -0,0 +1,29 @@ +import asyncio +import logging + +from .bot import bot +from .models.base import Base +from .settings import settings +from .storage.database import database +from .word_generator import WordGenerator + + +async def init_db() -> None: + """Seeds the tables.""" + async with database.engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +def main() -> None: + """Run the app.""" + logging.basicConfig(level=logging.INFO) + # TODO: move this to bot start hook + WordGenerator.download_corpus() + WordGenerator.boot() + + asyncio.run(init_db()) + bot.run(settings.DISCORD_TOKEN) + + +if __name__ == "__main__": + main() diff --git a/ornate-orbits/app/models/__init__.py b/ornate-orbits/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ornate-orbits/app/models/base.py b/ornate-orbits/app/models/base.py new file mode 100644 index 0000000..774eef6 --- /dev/null +++ b/ornate-orbits/app/models/base.py @@ -0,0 +1,45 @@ +from collections.abc import Callable, Sequence +from typing import Any + +from sqlalchemy import inspect +from sqlalchemy.orm import DeclarativeBase + + +class classproperty: # noqa: N801 + """Decorator that converts a method. + + with a single cls argument into a property + that can be accessed directly from the class. + """ + + def __init__(self, method: Callable[..., Any]) -> None: + self.fget = method + + def __get__(self, instance: Any, cls: type) -> Any: # noqa: ANN401 + return self.fget(cls) + + def getter(self, method: Callable[..., Any]) -> "classproperty": + """Return the method.""" + self.fget = method + return self + + +class Base(DeclarativeBase): + """Base model.""" + + @classproperty + def relationships(cls) -> Sequence[str]: # noqa: N805 + """Return all relationship columns.""" + return cls.__mapper__.relationships.keys() + + @classproperty + def columns(cls) -> Sequence[str]: # noqa: N805 + """Return all columns.""" + return [column.key for column in inspect(cls).mapper.attrs] + + def as_dict(self) -> dict[str, Any]: + """Return dictionary format.""" + return { + col.key: getattr(self, col.key) + for col in inspect(self).mapper.column_attrs + } diff --git a/ornate-orbits/app/models/guess.py b/ornate-orbits/app/models/guess.py new file mode 100644 index 0000000..9c37b69 --- /dev/null +++ b/ornate-orbits/app/models/guess.py @@ -0,0 +1,26 @@ +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base +from .wordle import Wordle + + +class Guess(Base): + """Guess model.""" + + __tablename__ = "guess" + + id: Mapped[UUID] = mapped_column( + primary_key=True, + index=True, + default=uuid4, + ) + created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) + content: Mapped[str] + result: Mapped[str] + wordle_id: Mapped[UUID] = mapped_column(ForeignKey("wordle.id")) + + wordle: Mapped[Wordle] = relationship(back_populates="guesses") diff --git a/ornate-orbits/app/models/player.py b/ornate-orbits/app/models/player.py new file mode 100644 index 0000000..67c7c6b --- /dev/null +++ b/ornate-orbits/app/models/player.py @@ -0,0 +1,13 @@ +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base + + +class Player(Base): + """Player model.""" + + __tablename__ = "player" + + id: Mapped[int] = mapped_column(primary_key=True, unique=True, index=True) + username: Mapped[str] + display_name: Mapped[str] diff --git a/ornate-orbits/app/models/trivia.py b/ornate-orbits/app/models/trivia.py new file mode 100644 index 0000000..28ccec6 --- /dev/null +++ b/ornate-orbits/app/models/trivia.py @@ -0,0 +1,34 @@ +from datetime import datetime +from enum import StrEnum, auto + +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base + + +class TriviaDifficulty(StrEnum): + """Enum for Trivia difficulties.""" + + EASY = auto() + MEDIUM = auto() + HARD = auto() + + +class Trivia(Base): + """Trivia model.""" + + __tablename__ = "trivia" + + id: Mapped[int] = mapped_column( + autoincrement=True, + primary_key=True, + index=True, + ) + created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) + difficulty: Mapped[TriviaDifficulty] + category: Mapped[str] + question: Mapped[str] + correct_answer: Mapped[str] + incorrect_answer_1: Mapped[str] + incorrect_answer_2: Mapped[str] + incorrect_answer_3: Mapped[str] diff --git a/ornate-orbits/app/models/wordle.py b/ornate-orbits/app/models/wordle.py new file mode 100644 index 0000000..9aaa7cd --- /dev/null +++ b/ornate-orbits/app/models/wordle.py @@ -0,0 +1,44 @@ +from datetime import datetime +from enum import Enum +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlalchemy import asc, text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base + +if TYPE_CHECKING: + from .guess import Guess + + +class WordleStatus(Enum): + """Status of Wordle.""" + + ACTIVE = 0 + COMPLETED = 1 + PENDING = 2 + ABORTED = 3 + + +class Wordle(Base): + """Wordle model.""" + + __tablename__ = "wordle" + + id: Mapped[UUID] = mapped_column( + primary_key=True, + index=True, + default=uuid4, + ) + created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) + word: Mapped[str] + user_id: Mapped[int] + status: Mapped[int] + + guesses: Mapped[list["Guess"]] = relationship( + back_populates="wordle", + order_by=asc(text("Guess.created_at")), + lazy="selectin", + ) diff --git a/ornate-orbits/app/settings.py b/ornate-orbits/app/settings.py new file mode 100644 index 0000000..41b7302 --- /dev/null +++ b/ornate-orbits/app/settings.py @@ -0,0 +1,13 @@ +import os +from typing import Final + + +class BotSettings: + """All settings for the Bot application goes here.""" + + COMMAND_PREFIX: Final[str] = os.getenv("COMMAND_PREFIX", "!@#") + DISCORD_TOKEN: Final[str] = os.environ["DISCORD_TOKEN"] + GUILD_ID: Final[str] = os.environ["GUILD_ID"] + + +settings = BotSettings() diff --git a/ornate-orbits/app/storage/__init__.py b/ornate-orbits/app/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ornate-orbits/app/storage/database.py b/ornate-orbits/app/storage/database.py new file mode 100644 index 0000000..339f921 --- /dev/null +++ b/ornate-orbits/app/storage/database.py @@ -0,0 +1,47 @@ +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from functools import cached_property +from typing import Any + +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) + + +class Database: + """Database wrapper.""" + + def __init__(self, url: str, **kw: dict[str, Any]) -> None: + self.engine = create_async_engine( + url, + pool_pre_ping=True, + echo=kw.pop("echo", False), + ) + + @cached_property + def session_maker(self) -> async_sessionmaker[AsyncSession]: + """Cached session maker.""" + return async_sessionmaker( + self.engine, + autoflush=False, + expire_on_commit=False, + ) + + @asynccontextmanager + async def create_session(self) -> AsyncIterator[AsyncSession]: + """Create a session context manager.""" + async with self.session_maker() as async_session: + try: + yield async_session + except Exception: + await async_session.rollback() + raise + finally: + await async_session.close() + + +# TODO: move this to a container +database = Database("sqlite+aiosqlite:///./test.db") +trivia_database = Database("sqlite+aiosqlite:///./trivia.db") diff --git a/ornate-orbits/app/storage/guess.py b/ornate-orbits/app/storage/guess.py new file mode 100644 index 0000000..0bc1d64 --- /dev/null +++ b/ornate-orbits/app/storage/guess.py @@ -0,0 +1,63 @@ +from collections.abc import Sequence +from typing import Any +from uuid import UUID + +from sqlalchemy import Result, select +from sqlalchemy.sql.functions import count + +from app.models.guess import Guess + +from .database import Database, database + + +class GuessRepo: + """Repository for interacting with Guess.""" + + def __init__(self, db: Database) -> None: + self.db: Database = db + + async def create( + self, + content: str, + result: str, + wordle_id: UUID, + ) -> Guess: + """Create a guess.""" + async with self.db.create_session() as session: + guess = Guess(content=content, result=result, wordle_id=wordle_id) + session.add(guess) + await session.commit() + await session.refresh(guess) + return guess + + async def get(self, id: UUID) -> Guess | None: + """Get guess by id.""" + async with self.db.create_session() as session: + stmt = select(Guess).where(Guess.id == id) + result = await session.execute(stmt) + guess: Guess | None = result.scalar() + return guess + + async def get_by_wordle_id(self, wordle_id: UUID) -> Sequence[Guess]: + """Get all guesses by wordle ID.""" + async with self.db.create_session() as session: + stmt = select(Guess).where(Guess.wordle_id == wordle_id) + result: Result[Any] = await session.execute(stmt) + guesses: Sequence[Guess] = result.scalars().all() + return guesses + + async def count_by_wordle_ids(self, wordle_ids: Sequence[UUID]) -> int: + """Count number of guesses by all wordle IDs.""" + async with self.db.create_session() as session: + stmt = ( + select(count()) + .select_from(Guess) + .where(Guess.wordle_id.in_(wordle_ids)) + ) + result: Result[Any] = await session.execute(stmt) + cnt: int = result.scalar() + return cnt + + +# TODO: move this to a container +guess_repo = GuessRepo(database) diff --git a/ornate-orbits/app/storage/player.py b/ornate-orbits/app/storage/player.py new file mode 100644 index 0000000..958967f --- /dev/null +++ b/ornate-orbits/app/storage/player.py @@ -0,0 +1,41 @@ +from sqlalchemy import select + +from app.models.player import Player + +from .database import Database, database + + +class PlayerRepo: + """Repository for players.""" + + def __init__(self, db: Database) -> None: + self.db: Database = db + + async def create( + self, + userid: int, + username: str, + display_name: str, + ) -> Player: + """Create a new player from discord user.""" + async with self.db.create_session() as session: + player = Player( + id=userid, + username=username, + display_name=display_name, + ) + session.add(player) + await session.commit() + await session.refresh(player) + return player + + async def get(self, id: int) -> Player | None: + """Get guess by id.""" + async with self.db.create_session() as session: + stmt = select(Player).where(Player.id == id) + result = await session.execute(stmt) + player: Player | None = result.scalar() + return player + + +player_repo = PlayerRepo(database) diff --git a/ornate-orbits/app/storage/trivia.py b/ornate-orbits/app/storage/trivia.py new file mode 100644 index 0000000..d7bd681 --- /dev/null +++ b/ornate-orbits/app/storage/trivia.py @@ -0,0 +1,36 @@ +from sqlalchemy import func, select + +from app.models.trivia import Trivia + +from .database import Database, trivia_database + + +class TriviaRepo: + """Repository for interacting with Trivia.""" + + def __init__(self, db: Database) -> None: + self.db: Database = db + + async def create( + self, + trivia: Trivia, + ) -> Trivia: + """Create a trivia.""" + async with self.db.create_session() as session: + session.add(trivia) + await session.commit() + await session.refresh(trivia) + return trivia + + async def get_random(self) -> Trivia: + """Get a random trivia question.""" + async with self.db.create_session() as session: + stmt = select(Trivia).order_by(func.random()) + result = await session.execute(stmt) + trivia: Trivia | None = result.scalar() + assert trivia, "no trivia in the database" + return trivia + + +# TODO: move this to a container +trivia_repo = TriviaRepo(trivia_database) diff --git a/ornate-orbits/app/storage/wordle.py b/ornate-orbits/app/storage/wordle.py new file mode 100644 index 0000000..2af0312 --- /dev/null +++ b/ornate-orbits/app/storage/wordle.py @@ -0,0 +1,174 @@ +import logging +from collections.abc import Sequence +from typing import Any, Final +from uuid import UUID + +from sqlalchemy import Result, desc, select, update + +from app.enums import MatchResult +from app.models.guess import Guess +from app.models.wordle import Wordle, WordleStatus + +from .database import Database, database + +logger = logging.getLogger(__name__) + + +class WordleNotFoundError(Exception): + """Wordle not found error.""" + + +class WordleRepo: + """Repository for interacting with Wordle.""" + + TRIVIA_THRESHOLD: Final[int] = 3 + + def __init__(self, db: Database) -> None: + self.db: Database = db + + async def create( + self, + word: str, + user_id: int, + ) -> Wordle: + """Create a wordle.""" + async with self.db.create_session() as session: + wordle = Wordle( + word=word, user_id=user_id, status=WordleStatus.ACTIVE.value + ) + session.add(wordle) + await session.commit() + await session.refresh(wordle) + return wordle + + async def get(self, id: UUID) -> Wordle | None: + """Get wordle by id.""" + async with self.db.create_session() as session: + stmt = select(Wordle).where(Wordle.id == id) + result = await session.execute(stmt) + wordle: Wordle | None = result.scalar() + return wordle + + async def get_by_user_id(self, user_id: int) -> Sequence[Wordle]: + """Get wordle by user id.""" + async with self.db.create_session() as session: + stmt = ( + select(Wordle) + .where(Wordle.user_id == user_id) + .order_by(desc(Wordle.created_at)) + ) + result: Result[Any] = await session.execute(stmt) + wordles: Sequence[Wordle] = result.scalars().all() + return wordles + + async def get_active_wordle_by_user_id( + self, + user_id: int, + ) -> Wordle | None: + """Get the active wordle by user id.""" + async with self.db.create_session() as session: + stmt = select(Wordle).where( + Wordle.user_id == user_id, + Wordle.status == WordleStatus.ACTIVE.value, + ) + + result = await session.execute(stmt) + wordle: Wordle | None = result.scalar() + return wordle + + async def get_pending_wordle(self, user_id: int) -> Wordle | None: + """Get the pending wordle by user id.""" + async with self.db.create_session() as session: + stmt = select(Wordle).where( + Wordle.user_id == user_id, + Wordle.status == WordleStatus.PENDING.value, + ) + result = await session.execute(stmt) + wordle: Wordle | None = result.scalar() + return wordle + + async def get_ongoing_wordle(self, user_id: int) -> Wordle | None: + """Get unfinished wordle game by user id.""" + async with self.db.create_session() as session: + stmt = select(Wordle).where( + Wordle.user_id == user_id, + Wordle.status.in_( + [ + WordleStatus.ACTIVE.value, + WordleStatus.PENDING.value, + ] + ), + ) + result = await session.execute(stmt) + wordle: Wordle | None = result.scalar() + return wordle + + async def _calculate_next_status(self, id: UUID) -> WordleStatus | None: + wordle = await wordle_repo.get(id=id) + if wordle is None: + return None + match wordle.status: + case WordleStatus.ACTIVE.value: + guesses = await wordle_repo.get_guesses(wordle.user_id) + if len(guesses) < self.TRIVIA_THRESHOLD: + return None + recent_results = [ + guess.result for guess in guesses[-self.TRIVIA_THRESHOLD :] + ] + if str( + MatchResult.CORRECT_LETTER_CORRECT_POSITION + ) not in "".join(recent_results): + return WordleStatus.PENDING.value + return None + case WordleStatus.PENDING.value: + return WordleStatus.ACTIVE.value + case WordleStatus.COMPLETED.value: + return None + case _: + raise ValueError + + async def change_status( + self, + id: UUID, + *, + is_winning: bool = False, + is_ending: bool = False, + ) -> None: + """Change the wordle status based on the current guess.""" + if is_winning and not is_ending: + next_status = WordleStatus.COMPLETED.value + elif is_ending and not is_winning: + next_status = WordleStatus.ABORTED.value + else: + next_status = await self._calculate_next_status(id) + if next_status is None: + return + logger.info("next status = %s", next_status) + async with self.db.create_session() as session: + stmt = ( + update(Wordle) + .where(Wordle.id == id) + .values(status=next_status) + ) + await session.execute(stmt) + await session.commit() + + async def win_game(self, id: UUID) -> None: + """Change the status when the play wins.""" + + async def get_guesses(self, user_id: int) -> Sequence[Guess]: + """Get the guesses of the active wordle of a user.""" + async with self.db.create_session() as session: + stmt = select(Wordle).where( + Wordle.user_id == user_id, + Wordle.status == WordleStatus.ACTIVE.value, + ) + result = await session.execute(stmt) + wordle: Wordle | None = result.scalar() + if wordle is None: + raise WordleNotFoundError + return wordle.guesses + + +# TODO: move this to a container +wordle_repo = WordleRepo(database) diff --git a/ornate-orbits/app/word_generator.py b/ornate-orbits/app/word_generator.py new file mode 100644 index 0000000..f708151 --- /dev/null +++ b/ornate-orbits/app/word_generator.py @@ -0,0 +1,190 @@ +import secrets +from dataclasses import dataclass +from enum import StrEnum +from functools import lru_cache +from typing import TYPE_CHECKING, Final + +import nltk + +if TYPE_CHECKING: + from nltk.corpus.reader.wordnet import Lemma, Synset + + +class Difficulty(StrEnum): + """Enum for game difficulties.""" + + EASY = "Easy" + MEDIUM = "Medium" + HARD = "Hard" + + +@dataclass +class Word: + """The dataclass for a Word.""" + + word: str + definition: str + synonyms: set[str] + usages: list[str] + + +class WordGenerator: + """The class for generating a random word between 5 to 10 chars.""" + + CORPORA_WORDNET: Final[str] = "corpora/wordnet" + WORDNET: Final[str] = "wordnet" + + WORD_LENGTH_MIN: Final[int] = 5 + WORD_LENGTH_MAX: Final[int] = 15 + + WORD_AMOUNT_HARD: Final[int] = 1 + WORD_AMOUNT_EASY: Final[int] = 5 + + def __init__(self) -> None: + self.download_corpus() + self.synsets: list[Synset] = [] + self.mp_len_words: dict[int, dict[Difficulty, list[str]]] = { + val: {diff: [] for diff in Difficulty} + for val in range(self.WORD_LENGTH_MIN, self.WORD_LENGTH_MAX + 1) + } + self.mp_len_synsets: dict[int, dict[Difficulty, list[Synset]]] = { + val: {diff: [] for diff in Difficulty} + for val in range(self.WORD_LENGTH_MIN, self.WORD_LENGTH_MAX + 1) + } + self.separate_lengths() + + def separate_lengths(self) -> None: + """Populate the wordnet data for each length.""" + from nltk.corpus import wordnet + + temp_words: dict[int, dict[str, int]] = {} + temp_synsets: dict[int, list[Synset]] = {} + + for synset in wordnet.all_synsets(): + word = synset.name().split(".", 1)[0] + if not self.is_qualified(word): + continue + word_length = len(word) + + temp_words.setdefault(word_length, {}) + if not temp_words.get(word_length, {}).get(word): + self.synsets.append(synset) + temp_words[word_length][word] = 1 + temp_synsets.setdefault(word_length, []) + temp_synsets[word_length].append(synset) + else: + temp_words[word_length][word] += 1 + + for i in range(self.WORD_LENGTH_MIN, self.WORD_LENGTH_MAX + 1): + for (word, val), synset in zip( + temp_words[i].items(), temp_synsets[i], strict=False + ): + if val == self.WORD_AMOUNT_HARD: + self.mp_len_words[i][Difficulty.HARD].append(word) + self.mp_len_synsets[i][Difficulty.HARD].append(synset) + elif self.WORD_AMOUNT_HARD < val < self.WORD_AMOUNT_EASY: + self.mp_len_words[i][Difficulty.MEDIUM].append(word) + self.mp_len_synsets[i][Difficulty.MEDIUM].append(synset) + else: + self.mp_len_words[i][Difficulty.EASY].append(word) + self.mp_len_synsets[i][Difficulty.EASY].append(synset) + + @classmethod + def boot(cls) -> None: + """Try accessing wordnet data to trigger data validation.""" + from nltk.corpus import wordnet + + list(wordnet.all_synsets()) + + @classmethod + def download_corpus(cls) -> None: + """Ensure the wordnet corpus is downloaded.""" + try: + nltk.data.find(cls.CORPORA_WORDNET) + except LookupError: + nltk.download(cls.WORDNET) + + def _random_word_in_synset(self) -> tuple[str, "Synset"]: + synset: Synset = secrets.choice(self.synsets) + lemma: Lemma = secrets.choice(synset.lemmas()) + return lemma.name(), synset + + def is_qualified(self, word: str) -> bool: + """Word qualification for adding into bank.""" + return ( + "-" not in word + and "_" not in word + and self.WORD_LENGTH_MIN <= len(word) <= self.WORD_LENGTH_MAX + ) + + def is_valid(self, word: str) -> bool: + """Check if the word exists in the bank.""" + if len(word) not in self.mp_len_words: + return False + return ( + word + in self.mp_len_words[len(word)][Difficulty.EASY] + + self.mp_len_words[len(word)][Difficulty.MEDIUM] + + self.mp_len_words[len(word)][Difficulty.HARD] + ) + + def random(self, length: int, difficulty: Difficulty) -> Word: + """Randomizes a word from the synset.""" + dataset: list[Synset] = self.mp_len_synsets.get(length, {}).get( + difficulty, [] + ) + + assert len(dataset) > 0, "the word bank is empty" + + synset = secrets.choice(dataset) + return Word( + word=synset.name().split(".", 1)[0], + definition=synset.definition(), + synonyms={lm.name() for lm in synset.lemmas()}, + usages=synset.examples(), + ) + + def get_word(self, word: str) -> Word: + """Get Word dataclass with the given word.""" + from nltk.corpus import wordnet + + synset: Synset = wordnet.synsets(word)[0] + + return Word( + word=synset.name().split(".", 1)[0], + definition=synset.definition(), + synonyms={lm.name() for lm in synset.lemmas()}, + usages=synset.examples(), + ) + + def __str__(self) -> str: + bank_stat = " | ".join( + [ + f"Len {length}: {len(words[diff])}" + for diff in Difficulty + for length, words in sorted(self.mp_len_words.items()) + ] + ) + return ( + "" + ) + + +@lru_cache +def get_wordgen() -> WordGenerator: + """Ensure only 1 instance of wordgen is used.""" + return WordGenerator() + + +if __name__ == "__main__": + import logging + + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + wordgen = WordGenerator() + logger.info(wordgen) diff --git a/ornate-orbits/build/app/Dockerfile b/ornate-orbits/build/app/Dockerfile new file mode 100644 index 0000000..99f6f1c --- /dev/null +++ b/ornate-orbits/build/app/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONUNBUFFERED=1 + +RUN pip install --upgrade pip setuptools + +RUN mkdir app +WORKDIR /app + +COPY ./app/ ./app/ +COPY ./trivia.db ./trivia.db +COPY ./requirements-dev.txt . + +RUN pip install watchdog==4.0.1 +RUN pip install -r requirements-dev.txt + +CMD ["bash", "./app/entrypoint.sh"] diff --git a/ornate-orbits/config/app/.env.example b/ornate-orbits/config/app/.env.example new file mode 100644 index 0000000..efceef9 --- /dev/null +++ b/ornate-orbits/config/app/.env.example @@ -0,0 +1,2 @@ +DISCORD_TOKEN= +GUILD_ID= diff --git a/ornate-orbits/docker-compose.yml b/ornate-orbits/docker-compose.yml new file mode 100644 index 0000000..fca35c6 --- /dev/null +++ b/ornate-orbits/docker-compose.yml @@ -0,0 +1,15 @@ +services: + bot: + build: + context: . + dockerfile: ./build/app/Dockerfile + env_file: + - ./config/app/.env + volumes: + - ./app:/app/app + restart: unless-stopped + stop_grace_period: 5s + develop: + watch: + - action: rebuild + path: requirements-dev.txt diff --git a/ornate-orbits/poetry.lock b/ornate-orbits/poetry.lock new file mode 100644 index 0000000..82223f2 --- /dev/null +++ b/ornate-orbits/poetry.lock @@ -0,0 +1,656 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "discord-py" +version = "2.4.0" +description = "A Python wrapper for the Discord API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"}, + {file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4,<4" + +[package.extras] +docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"] +speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"] +test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"] +voice = ["PyNaCl (>=1.3.0,<1.6)"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "identify" +version = "2.6.0" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "ruff" +version = "0.5.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.3-py3-none-linux_armv6l.whl", hash = "sha256:b12424d9db7347fa63c5ed9af010003338c63c629fb9c9c6adb2aa4f5699729b"}, + {file = "ruff-0.5.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8d72c5684bbd4ed304a9a955ee2e67f57b35f6193222ade910cca8a805490e3"}, + {file = "ruff-0.5.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d2fc2cdb85ccac1e816cc9d5d8cedefd93661bd957756d902543af32a6b04a71"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf4bc751240b2fab5d19254571bcacb315c7b0b00bf3c912d52226a82bbec073"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc697ec874fdd7c7ba0a85ec76ab38f8595224868d67f097c5ffc21136e72fcd"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e791d34d3557a3819b3704bc1f087293c821083fa206812842fa363f6018a192"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:76bb5a87fd397520b91a83eae8a2f7985236d42dd9459f09eef58e7f5c1d8316"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8cfc7a26422c78e94f1ec78ec02501bbad2df5834907e75afe474cc6b83a8c1"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96066c4328a49fce2dd40e80f7117987369feec30ab771516cf95f1cc2db923c"}, + {file = "ruff-0.5.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfe9ab5bdc0b08470c3b261643ad54ea86edc32b64d1e080892d7953add3ad"}, + {file = "ruff-0.5.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7704582a026fa02cca83efd76671a98ee6eb412c4230209efe5e2a006c06db62"}, + {file = "ruff-0.5.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:08058d077e21b856d32ebf483443390e29dc44d927608dc8f092ff6776519da9"}, + {file = "ruff-0.5.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77d49484429ed7c7e6e2e75a753f153b7b58f875bdb4158ad85af166a1ec1822"}, + {file = "ruff-0.5.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:642cbff6cbfa38d2566d8db086508d6f472edb136cbfcc4ea65997745368c29e"}, + {file = "ruff-0.5.3-py3-none-win32.whl", hash = "sha256:eafc45dd8bdc37a00b28e68cc038daf3ca8c233d73fea276dcd09defb1352841"}, + {file = "ruff-0.5.3-py3-none-win_amd64.whl", hash = "sha256:cbaec2ddf4f78e5e9ecf5456ea0f496991358a1d883862ed0b9e947e2b6aea93"}, + {file = "ruff-0.5.3-py3-none-win_arm64.whl", hash = "sha256:05fbd2cb404775d6cd7f2ff49504e2d20e13ef95fa203bd1ab22413af70d420b"}, + {file = "ruff-0.5.3.tar.gz", hash = "sha256:2a3eb4f1841771fa5b67a56be9c2d16fd3cc88e378bd86aaeaec2f7e6bcdd0a2"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "2.0" +python-versions = "3.12.*" +content-hash = "e4c463e872dddc7aeb1c70c42958b941c255029cea02f22ccfb59195259beaae" diff --git a/ornate-orbits/pyproject.toml b/ornate-orbits/pyproject.toml new file mode 100644 index 0000000..0c8ff11 --- /dev/null +++ b/ornate-orbits/pyproject.toml @@ -0,0 +1,69 @@ +[tool.poetry] +name = "code-jam-2024" +version = "0.1.0" +description = "Ornate Orbits @ Code Jam 2024" +authors = [] +license = "MIT" + +[tool.poetry.dependencies] +python = "3.12.*" +discord-py = "^2.4.0" + +[tool.poetry.dev-dependencies] +ruff = "~0.5.0" +pre-commit = "~3.7.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +# Increase the line length. This breaks PEP8 but it is way easier to work with. +# The original reason for this limit was a standard vim terminal is only 79 characters, +# but this doesn't really apply anymore. +line-length = 79 +# Target Python 3.12. If you decide to use a different version of Python +# you will need to update this value. +target-version = "py312" +# Automatically fix auto-fixable issues. +fix = true +# The directory containing the source code. If you choose a different project layout +# you will need to update this value. +src = ["src"] + +[tool.ruff.lint] +# Enable all linting rules. +select = ["ALL"] +# Ignore some of the most obnoxious linting errors. +ignore = [ + # Missing docstrings. + "D100", + "D104", + "D105", + "D106", + "D107", + # Docstring whitespace. + "D203", + "D213", + # Docstring punctuation. + "D415", + # Docstring quotes. + "D301", + # Imperative mood. + "D401", + # Builtins. + "A", + # Print statements. + "T20", + # TODOs. + "TD002", + "TD003", + "FIX", + # Annotations. + "ANN101", + "ANN102", + # Assert. + "S101", + "COM812", + "TRY401", +] diff --git a/ornate-orbits/requirements-dev.txt b/ornate-orbits/requirements-dev.txt new file mode 100644 index 0000000..2d33aba --- /dev/null +++ b/ornate-orbits/requirements-dev.txt @@ -0,0 +1,11 @@ +# This file contains all the development requirements for our linting toolchain. +# Don't forget to pin your dependencies! +# This list will have to be migrated if you wish to use another dependency manager. + +ruff~=0.5.0 +pre-commit~=3.7.1 + +discord.py~=2.4.0 +nltk~=3.8.1 +sqlalchemy~=2.0.28 +aiosqlite~=0.20.0 diff --git a/ornate-orbits/setup.cfg b/ornate-orbits/setup.cfg new file mode 100644 index 0000000..617edba --- /dev/null +++ b/ornate-orbits/setup.cfg @@ -0,0 +1,13 @@ +[mypy] +ignore_missing_imports = true +plugins = pydantic.mypy + +follow_imports = silent +warn_redundant_casts = True +warn_unused_ignores = True +disallow_any_generics = True +check_untyped_defs = True +no_implicit_reexport = True + +# for strict mypy: (this is the tricky one :-)) +disallow_untyped_defs = True diff --git a/ornate-orbits/tests/__init__.py b/ornate-orbits/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ornate-orbits/tests/test_wordle_game.py b/ornate-orbits/tests/test_wordle_game.py new file mode 100644 index 0000000..8d1750c --- /dev/null +++ b/ornate-orbits/tests/test_wordle_game.py @@ -0,0 +1,18 @@ +import unittest +from collections.abc import Generator + +from app.core.wordle import WordleGame + + +class TestWordleGame(unittest.TestCase): + """Tests for Wordle Game.""" + + def test_color_for_guess(self) -> None: + """Tests that matching colors are correct.""" + guess = "zehfq" + word = "hello" + + game = WordleGame() + colors = game.gen_colors_for_guess(guess, word) + assert isinstance(colors, Generator) + assert list(colors) == [4, 0, 1, 3, 2] diff --git a/ornate-orbits/trivia.db b/ornate-orbits/trivia.db new file mode 100644 index 0000000..3f80492 Binary files /dev/null and b/ornate-orbits/trivia.db differ