diff --git a/.flake8 b/.flake8 index 259dc4e..3b71da3 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,4 @@ [flake8] -# Don't lint non-Python files exclude = .git, .github, @@ -10,30 +9,10 @@ exclude = htmlcov, tests, venv -# Disable rules that we don't care about (or conflict with others) -extend-ignore = - # Missing docstring in public module - D100, D104, - # Missing docstring in magic method - D105, - # Missing docstring in __init__ - D107, - # First line of docstrings should end with a period - D400, - # First line of docstrings should be in imperative mood - D401, - # Whitespace before ":" - E203, -# Don't require docstrings when overriding a method, -# the base method should have a docstring but the rest not -ignore-decorator=overrides +ignore=E203 max-line-length = 120 -# Disable some rules for entire files per-file-ignores = # Missing __all__, main isn't supposed to be imported main.py: DALL000, # Missing __all__, Cogs aren't modules - ./didier/cogs/*: DALL000, - # All of the colours methods are just oneliners to create a colour, - # there's no point adding docstrings (function names are enough) - ./didier/utils/discord/colours.py: D103 + ./didier/cogs/*: DALL000 \ No newline at end of file diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 75e7e92..b083e02 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -59,8 +59,6 @@ jobs: coverage xml - name: Upload coverage report to CodeCov uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV }} linting: needs: [dependencies] runs-on: ubuntu-latest diff --git a/codecov.yaml b/codecov.yaml index e916d57..6ce17c3 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -10,5 +10,5 @@ coverage: precision: 5 ignore: - - "./didier/cogs/*" # Cogs can't really be tested properly - "./tests/*" + - "./didier/cogs/*" # Cogs can't really be tested properly diff --git a/database/crud/ufora_announcements.py b/database/crud/ufora_announcements.py index 48a06ae..5f82390 100644 --- a/database/crud/ufora_announcements.py +++ b/database/crud/ufora_announcements.py @@ -28,7 +28,6 @@ async def create_new_announcement( async def remove_old_announcements(session: AsyncSession): """Delete all announcements that are > 8 days old - The RSS feed only goes back 7 days, so all of these old announcements never have to be checked again when checking if an announcement is fresh or not. """ diff --git a/database/crud/ufora_courses.py b/database/crud/ufora_courses.py deleted file mode 100644 index d41846c..0000000 --- a/database/crud/ufora_courses.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Optional - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from database.models import UforaCourse, UforaCourseAlias - -__all__ = ["get_all_courses", "get_course_by_name"] - - -async def get_all_courses(session: AsyncSession) -> list[UforaCourse]: - """Get a list of all courses in the database""" - statement = select(UforaCourse) - return list((await session.execute(statement)).scalars().all()) - - -async def get_course_by_name(session: AsyncSession, query: str) -> Optional[UforaCourse]: - """Try to find a course by its name - - This checks for regular name first, and then aliases - """ - # Search case-insensitively - query = query.lower() - - statement = select(UforaCourse).where(UforaCourse.name.ilike(f"%{query}%")) - result = (await session.execute(statement)).scalars().first() - if result: - return result - - statement = select(UforaCourseAlias).where(UforaCourseAlias.alias.ilike(f"%{query}%")) - result = (await session.execute(statement)).scalars().first() - return result.course if result else None diff --git a/database/crud/users.py b/database/crud/users.py index 57c5029..cc5a899 100644 --- a/database/crud/users.py +++ b/database/crud/users.py @@ -12,8 +12,7 @@ __all__ = [ async def get_or_add(session: AsyncSession, user_id: int) -> User: """Get a user's profile - - If it doesn't exist yet, create it (along with all linked datastructures) + If it doesn't exist yet, create it (along with all linked datastructures). """ statement = select(User).where(User.user_id == user_id) user: Optional[User] = (await session.execute(statement)).scalar_one_or_none() diff --git a/database/utils/caches.py b/database/utils/caches.py deleted file mode 100644 index 5e5be4f..0000000 --- a/database/utils/caches.py +++ /dev/null @@ -1,74 +0,0 @@ -from abc import ABC, abstractmethod - -from sqlalchemy.ext.asyncio import AsyncSession - -from database.crud import ufora_courses - -__all__ = ["CacheManager"] - - -class DatabaseCache(ABC): - """Base class for a simple cache-like structure - - The goal of this class is to store data for Discord auto-completion results - that would otherwise potentially put heavy load on the database. - - This only stores strings, to avoid having to constantly refresh these objects. - Once a choice has been made, it can just be pulled out of the database. - - Considering the fact that a user isn't obligated to choose something from the suggestions, - chances are high we have to go to the database for the final action either way. - - Also stores the data in lowercase to allow fast searching - """ - - data: list[str] = [] - data_transformed: list[str] = [] - - def clear(self): - """Remove everything""" - self.data.clear() - - @abstractmethod - async def refresh(self, database_session: AsyncSession): - """Refresh the data stored in this cache""" - - async def invalidate(self, database_session: AsyncSession): - """Invalidate the data stored in this cache""" - await self.refresh(database_session) - - def get_autocomplete_suggestions(self, query: str): - """Filter the cache to find everything that matches the search query""" - query = query.lower() - # Return the original (non-transformed) version of the data for pretty display in Discord - return [self.data[index] for index, value in enumerate(self.data_transformed) if query in value] - - -class UforaCourseCache(DatabaseCache): - """Cache to store the names of Ufora courses""" - - async def refresh(self, database_session: AsyncSession): - self.clear() - - courses = await ufora_courses.get_all_courses(database_session) - - # Load the course names + all the aliases - for course in courses: - aliases = list(map(lambda x: x.alias, course.aliases)) - self.data.extend([course.name, *aliases]) - - self.data.sort() - self.data_transformed = list(map(str.lower, self.data)) - - -class CacheManager: - """Class that keeps track of all caches""" - - ufora_courses: UforaCourseCache - - def __init__(self): - self.ufora_courses = UforaCourseCache() - - async def initialize_caches(self, database_session: AsyncSession): - """Initialize the contents of all caches""" - await self.ufora_courses.refresh(database_session) diff --git a/didier/cogs/help.py b/didier/cogs/help.py index 77ae0f1..33250a8 100644 --- a/didier/cogs/help.py +++ b/didier/cogs/help.py @@ -2,15 +2,13 @@ from typing import List, Mapping, Optional import discord from discord.ext import commands -from overrides import overrides from didier import Didier class CustomHelpCommand(commands.MinimalHelpCommand): """Customised Help command to override the default implementation - - The default is ugly as hell so we do some fiddling with it + The default is ugly as hell """ def _help_embed_base(self, title: str) -> discord.Embed: @@ -32,7 +30,6 @@ class CustomHelpCommand(commands.MinimalHelpCommand): return list(sorted(filtered_cogs, key=lambda cog: cog.qualified_name)) - @overrides async def send_bot_help(self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]], /): embed = self._help_embed_base("Categorieën") filtered_cogs = await self._filter_cogs(list(mapping.keys())) diff --git a/didier/cogs/other.py b/didier/cogs/other.py deleted file mode 100644 index 6a036e2..0000000 --- a/didier/cogs/other.py +++ /dev/null @@ -1,24 +0,0 @@ -from discord.ext import commands - -from didier import Didier -from didier.data.apis import urban_dictionary - - -class Other(commands.Cog): - """Cog for commands that don't really belong anywhere else""" - - client: Didier - - def __init__(self, client: Didier): - self.client = client - - @commands.hybrid_command(name="define", description="Urban Dictionary", aliases=["Ud", "Urban"], usage="[Woord]") - async def define(self, ctx: commands.Context, *, query: str): - """Look up the definition of a word on the Urban Dictionary""" - definitions = await urban_dictionary.lookup(self.client.http_session, query) - await ctx.reply(embed=definitions[0].to_embed(), mention_author=False) - - -async def setup(client: Didier): - """Load the cog""" - await client.add_cog(Other(client)) diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index ca72cd6..401067c 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -25,9 +25,8 @@ class Owner(commands.Cog): self.client = client async def cog_check(self, ctx: commands.Context) -> bool: - """Global check for every command in this cog - - This means that we don't have to add is_owner() to every single command separately + """Global check for every command in this cog, so we don't have to add + is_owner() to every single command separately """ # pylint: disable=W0236 # Pylint thinks this can't be async, but it can return await self.client.is_owner(ctx.author) diff --git a/didier/data/apis/__init__.py b/didier/data/apis/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/didier/data/apis/urban_dictionary.py b/didier/data/apis/urban_dictionary.py deleted file mode 100644 index 40381ea..0000000 --- a/didier/data/apis/urban_dictionary.py +++ /dev/null @@ -1,17 +0,0 @@ -from aiohttp import ClientSession - -from didier.data.embeds.urban_dictionary import Definition - -__all__ = ["lookup", "PER_PAGE"] - - -PER_PAGE = 10 - - -async def lookup(http_session: ClientSession, query: str) -> list[Definition]: - """Fetch the Urban Dictionary definitions for a given word""" - url = "https://api.urbandictionary.com/v0/define" - - async with http_session.get(url, params={"term": query}) as response: - response_json = await response.json() - return list(map(Definition.parse_obj, response_json["list"])) diff --git a/didier/data/embeds/base.py b/didier/data/embeds/base.py deleted file mode 100644 index 8cab519..0000000 --- a/didier/data/embeds/base.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import ABC, abstractmethod - -import discord -from pydantic import BaseModel - -__all__ = [ - "EmbedBaseModel", - "EmbedPydantic", -] - - -class EmbedBaseModel(ABC): - """Abstract base class for a model that can be turned into a Discord embed""" - - @abstractmethod - def to_embed(self) -> discord.Embed: - """Turn this model into a Discord embed""" - raise NotImplementedError - - -class EmbedPydantic(EmbedBaseModel, BaseModel, ABC): - """Pydantic version of EmbedModel""" diff --git a/didier/data/embeds/ufora/announcements.py b/didier/data/embeds/ufora/announcements.py index f4a8bdd..984674e 100644 --- a/didier/data/embeds/ufora/announcements.py +++ b/didier/data/embeds/ufora/announcements.py @@ -14,7 +14,6 @@ from sqlalchemy.ext.asyncio import AsyncSession import settings from database.crud import ufora_announcements as crud from database.models import UforaCourse -from didier.data.embeds.base import EmbedBaseModel from didier.utils.types.datetime import int_to_weekday from didier.utils.types.string import leading @@ -26,7 +25,7 @@ __all__ = [ @dataclass -class UforaNotification(EmbedBaseModel): +class UforaNotification: """A single notification from Ufora""" content: dict diff --git a/didier/data/embeds/urban_dictionary.py b/didier/data/embeds/urban_dictionary.py deleted file mode 100644 index 14086bb..0000000 --- a/didier/data/embeds/urban_dictionary.py +++ /dev/null @@ -1,62 +0,0 @@ -from datetime import datetime - -import discord -from overrides import overrides -from pydantic import validator - -from didier.data.embeds.base import EmbedPydantic -from didier.utils.discord import colours -from didier.utils.discord.constants import Limits -from didier.utils.types import string as string_utils - -__all__ = ["Definition"] - - -class Definition(EmbedPydantic): - """A definition from the Urban Dictionary""" - - word: str - definition: str - example: str - permalink: str - author: str - thumbs_up: int - thumbs_down: int - written_on: datetime - - @property - def ratio(self) -> float: - """The up vote/down vote ratio - - This ratio is rounded down to 2 decimal places - If the amount of down votes is 0, always return 100% - """ - # No down votes, possibly no up votes either - # Avoid a 0/0 situation - if self.thumbs_down == 0: - return 100 - - total_votes = self.thumbs_up + self.thumbs_down - return round(100 * self.thumbs_up / total_votes, 2) - - @validator("definition", "example") - def modify_long_text(cls, field): - """Remove brackets from fields & cut them off if they are too long""" - field = field.replace("[", "").replace("]", "") - return string_utils.abbreviate(field, max_length=Limits.EMBED_FIELD_VALUE_LENGTH) - - @overrides - def to_embed(self) -> discord.Embed: - embed = discord.Embed(colour=colours.urban_dictionary_green()) - embed.set_author(name="Urban Dictionary") - - embed.add_field(name="Woord", value=self.word, inline=True) - embed.add_field(name="Auteur", value=self.author, inline=True) - embed.add_field(name="Definitie", value=self.definition, inline=False) - embed.add_field(name="Voorbeeld", value=self.example or "\u200B", inline=False) - embed.add_field( - name="Rating", value=f"{self.ratio}% ({self.thumbs_up}/{self.thumbs_up + self.thumbs_down})", inline=True - ) - embed.add_field(name="Link", value=f"[Urban Dictionary]({self.permalink})", inline=True) - - return embed diff --git a/didier/data/modals/custom_commands.py b/didier/data/modals/custom_commands.py index 35ac158..65c259f 100644 --- a/didier/data/modals/custom_commands.py +++ b/didier/data/modals/custom_commands.py @@ -2,7 +2,6 @@ import traceback import typing import discord -from overrides import overrides from database.crud.custom_commands import create_command, edit_command from didier import Didier @@ -25,14 +24,12 @@ class CreateCustomCommand(discord.ui.Modal, title="Create Custom Command"): super().__init__(*args, **kwargs) self.client = client - @overrides async def on_submit(self, interaction: discord.Interaction): async with self.client.db_session as session: command = await create_command(session, str(self.name.value), str(self.response.value)) await interaction.response.send_message(f"Successfully created ``{command.name}``.", ephemeral=True) - @overrides async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore await interaction.response.send_message("Something went wrong.", ephemeral=True) traceback.print_tb(error.__traceback__) @@ -40,8 +37,7 @@ class CreateCustomCommand(discord.ui.Modal, title="Create Custom Command"): class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"): """Modal to edit an existing custom command - - Fills in the current values as defaults for QOL + Fills in the current values as defaults """ name: discord.ui.TextInput @@ -63,7 +59,6 @@ class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"): ) ) - @overrides async def on_submit(self, interaction: discord.Interaction): name_field = typing.cast(discord.ui.TextInput, self.children[0]) response_field = typing.cast(discord.ui.TextInput, self.children[1]) @@ -73,7 +68,6 @@ class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"): await interaction.response.send_message(f"Successfully edited ``{self.original_name}``.", ephemeral=True) - @overrides async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore await interaction.response.send_message("Something went wrong.", ephemeral=True) traceback.print_tb(error.__traceback__) diff --git a/didier/didier.py b/didier/didier.py index 9fef227..14a9943 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession import settings from database.crud import custom_commands from database.engine import DBSession -from database.utils.caches import CacheManager from didier.utils.discord.prefix import get_prefix __all__ = ["Didier"] @@ -17,7 +16,6 @@ __all__ = ["Didier"] class Didier(commands.Bot): """DIDIER <3""" - database_caches: CacheManager initial_extensions: tuple[str, ...] = () http_session: ClientSession @@ -44,19 +42,11 @@ class Didier(commands.Bot): return DBSession() async def setup_hook(self) -> None: - """Do some initial setup - - This hook is called once the bot is initialised - """ + """Hook called once the bot is initialised""" # Load extensions await self._load_initial_extensions() await self._load_directory_extensions("didier/cogs") - # Initialize caches - self.database_caches = CacheManager() - async with self.db_session as session: - await self.database_caches.initialize_caches(session) - # Create aiohttp session self.http_session = ClientSession() @@ -123,9 +113,7 @@ class Didier(commands.Bot): async def _try_invoke_custom_command(self, message: discord.Message) -> bool: """Check if the message tries to invoke a custom command - If it does, send the reply associated with it - Returns a boolean indicating if a message invoked a command or not """ # Doesn't start with the custom command prefix if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX): diff --git a/didier/exceptions/__init__.py b/didier/exceptions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/didier/exceptions/config.py b/didier/exceptions/config.py deleted file mode 100644 index df73f7e..0000000 --- a/didier/exceptions/config.py +++ /dev/null @@ -1,12 +0,0 @@ -__all__ = ["MissingEnvironmentVariable"] - - -class MissingEnvironmentVariable(RuntimeError): - """Exception raised when an environment variable is missing - - These are not necessarily checked on startup, because they may be unused - during a given test run, and random unrelated crashes would be annoying - """ - - def __init__(self, variable: str): - super().__init__(f"Missing environment variable: {variable}") diff --git a/didier/utils/discord/colours.py b/didier/utils/discord/colours.py deleted file mode 100644 index ffcd8eb..0000000 --- a/didier/utils/discord/colours.py +++ /dev/null @@ -1,7 +0,0 @@ -import discord - -__all__ = ["urban_dictionary_green"] - - -def urban_dictionary_green() -> discord.Colour: - return discord.Colour.from_rgb(220, 255, 0) diff --git a/didier/utils/discord/constants.py b/didier/utils/discord/constants.py deleted file mode 100644 index 707d635..0000000 --- a/didier/utils/discord/constants.py +++ /dev/null @@ -1,17 +0,0 @@ -from enum import Enum - -__all__ = ["Limits"] - - -class Limits(int, Enum): - """Enum for the limits of certain fields""" - - EMBED_AUTHOR_LENGTH = 256 - EMBED_DESCRIPTION_LENGTH = 4096 - EMBED_FIELD_COUNT = 25 - EMBED_FIELD_NAME_LENGTH = 256 - EMBED_FIELD_VALUE_LENGTH = 1024 - EMBED_FOOTER_LENGTH = 2048 - EMBED_TITLE_LENGTH = 256 - EMBED_TOTAL_LENGTH = 6000 - MESSAGE_LENGTH = 2000 diff --git a/didier/utils/discord/converters/numbers.py b/didier/utils/discord/converters/numbers.py index 61d991a..573652f 100644 --- a/didier/utils/discord/converters/numbers.py +++ b/didier/utils/discord/converters/numbers.py @@ -6,7 +6,6 @@ __all__ = ["abbreviated_number"] def abbreviated_number(argument: str) -> Union[str, int]: """Custom converter to allow numbers to be abbreviated - Examples: 515k 4m diff --git a/didier/utils/discord/menus/__init__.py b/didier/utils/discord/menus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/didier/utils/discord/prefix.py b/didier/utils/discord/prefix.py index df62ad4..f57e06d 100644 --- a/didier/utils/discord/prefix.py +++ b/didier/utils/discord/prefix.py @@ -10,7 +10,6 @@ __all__ = ["get_prefix"] def get_prefix(client: commands.Bot, message: Message) -> str: """Match a prefix against a message - This is done dynamically to allow variable amounts of whitespace, and through regexes to allow case-insensitivity among other things. """ diff --git a/didier/utils/types/string.py b/didier/utils/types/string.py index 015996a..ab4f415 100644 --- a/didier/utils/types/string.py +++ b/didier/utils/types/string.py @@ -1,24 +1,11 @@ import math from typing import Optional -__all__ = ["abbreviate", "leading", "pluralize"] - - -def abbreviate(text: str, max_length: int) -> str: - """Abbreviate a string to a maximum length - - If the string is longer, add an ellipsis (...) at the end - """ - if len(text) <= max_length: - return text - - # Strip to avoid ending on random double newlines - return text[: max_length - 1].strip() + "…" +__all__ = ["leading", "pluralize"] def leading(character: str, string: str, target_length: Optional[int] = 2) -> str: """Add a leading [character] to [string] to make it length [target_length] - Pass None to target length to always do it (once), no matter the length """ # Cast to string just in case diff --git a/pyproject.toml b/pyproject.toml index 2a28e94..ba0c283 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,7 @@ omit = [ "./database/migrations.py", "./didier/cogs/*", "./didier/didier.py", - "./didier/data/*", - "./didier/utils/discord/colours.py" + "./didier/data/*" ] [tool.isort] @@ -23,7 +22,6 @@ profile = "black" [tool.mypy] plugins = [ - "pydantic.mypy", "sqlalchemy.ext.mypy.plugin" ] [[tool.mypy.overrides]] diff --git a/readme.md b/readme.md index 58e55e9..2c2e678 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,5 @@ # Didier -[![wakatime](https://wakatime.com/badge/user/3543d4ec-ec93-4b43-abd6-2bc2e310f3c4/project/100156e4-2fb5-40b4-b808-e47ef687905c.svg)](https://wakatime.com/badge/user/3543d4ec-ec93-4b43-abd6-2bc2e310f3c4/project/100156e4-2fb5-40b4-b808-e47ef687905c) - You bet. The time has come. ### Discord Documentation diff --git a/requirements-dev.txt b/requirements-dev.txt index 533740c..6c6e38e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,8 +11,8 @@ types-pytz==2021.3.8 # Flake8 + plugins flake8==4.0.1 flake8-bandit==3.0.0 +flake8-black==0.3.3 flake8-bugbear==22.7.1 -flake8-docstrings==1.6.0 flake8-dunder-all==0.2.1 flake8-eradicate==1.2.1 flake8-isort==4.1.1 diff --git a/requirements.txt b/requirements.txt index 0737e1f..1b4240a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,5 @@ git+https://github.com/Rapptz/discord.py environs==9.5.0 feedparser==6.0.10 markdownify==0.11.2 -overrides==6.1.0 -pydantic==1.9.1 -python-dateutil==2.8.2 pytz==2022.1 sqlalchemy[asyncio]==1.4.37 diff --git a/settings.py b/settings.py index bc35ab4..d850eb8 100644 --- a/settings.py +++ b/settings.py @@ -6,26 +6,6 @@ from environs import Env env = Env() env.read_env() -__all__ = [ - "SANDBOX", - "LOGFILE", - "DB_NAME", - "DB_USERNAME", - "DB_PASSWORD", - "DB_HOST", - "DB_PORT", - "DISCORD_TOKEN", - "DISCORD_READY_MESSAGE", - "DISCORD_STATUS_MESSAGE", - "DISCORD_TEST_GUILDS", - "DISCORD_BOOS_REACT", - "DISCORD_CUSTOM_COMMAND_PREFIX", - "UFORA_ANNOUNCEMENTS_CHANNEL", - "UFORA_RSS_TOKEN", - "URBAN_DICTIONARY_TOKEN", -] - - """General config""" SANDBOX: bool = env.bool("SANDBOX", True) LOGFILE: str = env.str("LOGFILE", "didier.log") @@ -38,14 +18,13 @@ DB_HOST: str = env.str("DB_HOST", "localhost") DB_PORT: int = env.int("DB_PORT", "5432") """Discord""" -DISCORD_TOKEN: str = env.str("DISCORD_TOKEN") -DISCORD_READY_MESSAGE: str = env.str("DISCORD_READY_MESSAGE", "I'M READY I'M READY I'M READY") -DISCORD_STATUS_MESSAGE: str = env.str("DISCORD_STATUS_MESSAGE", "with your Didier Dinks.") -DISCORD_TEST_GUILDS: list[int] = env.list("DISCORD_TEST_GUILDS", [], subcast=int) -DISCORD_BOOS_REACT: str = env.str("DISCORD_BOOS_REACT", "<:boos:629603785840263179>") -DISCORD_CUSTOM_COMMAND_PREFIX: str = env.str("DISCORD_CUSTOM_COMMAND_PREFIX", "?") +DISCORD_TOKEN: str = env.str("DISC_TOKEN") +DISCORD_READY_MESSAGE: str = env.str("DISC_READY_MESSAGE", "I'M READY I'M READY I'M READY") +DISCORD_STATUS_MESSAGE: str = env.str("DISC_STATUS_MESSAGE", "with your Didier Dinks.") +DISCORD_TEST_GUILDS: list[int] = env.list("DISC_TEST_GUILDS", [], subcast=int) +DISCORD_BOOS_REACT: str = env.str("DISC_BOOS_REACT", "<:boos:629603785840263179>") +DISCORD_CUSTOM_COMMAND_PREFIX: str = env.str("DISC_CUSTOM_COMMAND_PREFIX", "?") UFORA_ANNOUNCEMENTS_CHANNEL: Optional[int] = env.int("UFORA_ANNOUNCEMENTS_CHANNEL", None) """API Keys""" UFORA_RSS_TOKEN: Optional[str] = env.str("UFORA_RSS_TOKEN", None) -URBAN_DICTIONARY_TOKEN: Optional[str] = env.str("URBAN_DICTIONARY_TOKEN", None) diff --git a/tests/conftest.py b/tests/conftest.py index c8ab65f..b2a1e04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ import asyncio -import datetime from typing import AsyncGenerator, Generator from unittest.mock import MagicMock @@ -7,11 +6,9 @@ import pytest from sqlalchemy.ext.asyncio import AsyncSession from database.engine import engine -from database.models import Base, UforaAnnouncement, UforaCourse, UforaCourseAlias +from database.models import Base from didier import Didier -"""General fixtures""" - @pytest.fixture(scope="session") def event_loop() -> Generator: @@ -57,34 +54,3 @@ def mock_client() -> Didier: mock_client.user = mock_user return mock_client - - -"""Fixtures to put fake data in the database""" - - -@pytest.fixture -async def ufora_course(database_session: AsyncSession) -> UforaCourse: - """Fixture to create a course""" - course = UforaCourse(name="test", code="code", year=1, log_announcements=True) - database_session.add(course) - await database_session.commit() - return course - - -@pytest.fixture -async def ufora_course_with_alias(database_session: AsyncSession, ufora_course: UforaCourse) -> UforaCourse: - """Fixture to create a course with an alias""" - alias = UforaCourseAlias(course_id=ufora_course.course_id, alias="alias") - database_session.add(alias) - await database_session.commit() - await database_session.refresh(ufora_course) - return ufora_course - - -@pytest.fixture -async def ufora_announcement(ufora_course: UforaCourse, database_session: AsyncSession) -> UforaAnnouncement: - """Fixture to create an announcement""" - announcement = UforaAnnouncement(course_id=ufora_course.course_id, publication_date=datetime.datetime.now()) - database_session.add(announcement) - await database_session.commit() - return announcement diff --git a/tests/test_data/urban_dictionary_response.json b/tests/test_data/urban_dictionary_response.json deleted file mode 100644 index 071a880..0000000 --- a/tests/test_data/urban_dictionary_response.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "list": [ - { - "definition": "It literally means covfefe.\n\nOriginated from [Donald Trump's] [tweet]: \"Despite the constant [negative press] covfefe\"", - "permalink": "http://covfefe.urbanup.com/11630874", - "thumbs_up": 14701, - "sound_urls": [], - "author": "lightinglax", - "word": "covfefe", - "defid": 11630874, - "current_vote": "", - "written_on": "2017-05-31T04:54:39.112Z", - "example": "\"[It's time] to [nuke this] place down.\" \"What's [the code]?\" \"covfefe.\"", - "thumbs_down": 2337 - }, - { - "definition": "An [unpresidented] [typo]", - "permalink": "http://covfefe.urbanup.com/11631018", - "thumbs_up": 2295, - "sound_urls": [], - "author": "John Wilkes Bluetooth", - "word": "covfefe", - "defid": 11631018, - "current_vote": "", - "written_on": "2017-05-31T05:21:10.475Z", - "example": "[Despite] the [constant] [negative press] covfefe", - "thumbs_down": 776 - }, - { - "definition": "Originally coined by Donald Trump, [45th President of the United States] of America, covfefe will inevitably come to be syonymous with sending a text or [publishing] a tweet prematurely and with [egregious] spelling errors.", - "permalink": "http://covfefe.urbanup.com/11631031", - "thumbs_up": 3182, - "sound_urls": [], - "author": "covfefe89", - "word": "covfefe", - "defid": 11631031, - "current_vote": "", - "written_on": "2017-05-31T05:23:13.478Z", - "example": "Damn dog, I just covfefed tryna ask Ashley out. I meant to text her to get a drink but instead i wrote \"despite the constant [negative press] [convfefe].\" She's gonna think I'm [mad stupid] now.", - "thumbs_down": 1325 - }, - { - "definition": "¯\\_(ツ)_/¯", - "permalink": "http://covfefe.urbanup.com/11630896", - "thumbs_up": 245, - "sound_urls": [], - "author": "zanzalaz", - "word": "covfefe", - "defid": 11630896, - "current_vote": "", - "written_on": "2017-05-31T04:56:45.441Z", - "example": "\"[Despite] the [constant] [negative press] covfefe\"", - "thumbs_down": 105 - }, - { - "definition": "The [nuclear codes].", - "permalink": "http://covfefe.urbanup.com/11630856", - "thumbs_up": 830, - "sound_urls": [], - "author": "gosty7", - "word": "covfefe", - "defid": 11630856, - "current_vote": "", - "written_on": "2017-05-31T04:52:01.695Z", - "example": "[Despite] the [constant] [negative press] covfefe", - "thumbs_down": 445 - }, - { - "definition": "No one really knows yet, but the President of the United States of America used it in a tweet so it must be a [bigly] important word. It must be [YUGE] somewhere as he knows [all the best words].", - "permalink": "http://covfefe.urbanup.com/11630902", - "thumbs_up": 2711, - "sound_urls": [], - "author": "CovfefeFan", - "word": "covfefe", - "defid": 11630902, - "current_vote": "", - "written_on": "2017-05-31T04:57:30.841Z", - "example": "[Despite] the [constant] [negative press] covfefe", - "thumbs_down": 1612 - }, - { - "definition": "an [alternative fact] for [the word] 'coverage'", - "permalink": "http://covfefe.urbanup.com/11631148", - "thumbs_up": 92, - "sound_urls": [], - "author": "ceckhardt", - "word": "covfefe", - "defid": 11631148, - "current_vote": "", - "written_on": "2017-05-31T05:51:21.446Z", - "example": "\"[Despite] [constant] [negative press] covfefe\"", - "thumbs_down": 52 - }, - { - "definition": "Acronym : descriptive [--] Can’t [Operate] Very Fucking Efficiently [Faking] Effectiveness", - "permalink": "http://covfefe.urbanup.com/11634131", - "thumbs_up": 40, - "sound_urls": [], - "author": "Robert the Punster", - "word": "covfefe", - "defid": 11634131, - "current_vote": "", - "written_on": "2017-05-31T21:36:36.704Z", - "example": "[Some people] covfefe", - "thumbs_down": 22 - }, - { - "definition": "A word [Trump] [tweeted] and [nobody knows] what the hell it means.", - "permalink": "http://covfefe.urbanup.com/11630891", - "thumbs_up": 105, - "sound_urls": [], - "author": "pmince", - "word": "covfefe", - "defid": 11630891, - "current_vote": "", - "written_on": "2017-05-31T04:56:21.018Z", - "example": "[Despite] the [constant] [negative press] covfefe", - "thumbs_down": 75 - }, - { - "definition": "(N) Word used to describe a person with so much [egotism] that he/she cannot [admit] he/she made even the simplest of [mistakes].", - "permalink": "http://covfefe.urbanup.com/11636070", - "thumbs_up": 28, - "sound_urls": [], - "author": "PJ the Coug", - "word": "covfefe", - "defid": 11636070, - "current_vote": "", - "written_on": "2017-06-01T06:33:01.986Z", - "example": "\"[Kevin] [fell asleep] playing online last night. He just said he was 'afk' for [four] hours. He's got a lot of covfefe.\"", - "thumbs_down": 17 - } - ] -} \ No newline at end of file diff --git a/tests/test_database/test_crud/test_ufora_announcements.py b/tests/test_database/test_crud/test_ufora_announcements.py index b2385a2..ba6564a 100644 --- a/tests/test_database/test_crud/test_ufora_announcements.py +++ b/tests/test_database/test_crud/test_ufora_announcements.py @@ -7,6 +7,24 @@ from database.crud import ufora_announcements as crud from database.models import UforaAnnouncement, UforaCourse +@pytest.fixture +async def course(database_session: AsyncSession) -> UforaCourse: + """Fixture to create a course""" + course = UforaCourse(name="test", code="code", year=1, log_announcements=True) + database_session.add(course) + await database_session.commit() + return course + + +@pytest.fixture +async def announcement(course: UforaCourse, database_session: AsyncSession) -> UforaAnnouncement: + """Fixture to create an announcement""" + announcement = UforaAnnouncement(course_id=course.course_id, publication_date=datetime.datetime.now()) + database_session.add(announcement) + await database_session.commit() + return announcement + + async def test_get_courses_with_announcements_none(database_session: AsyncSession): """Test getting all courses with announcements when there are none""" results = await crud.get_courses_with_announcements(database_session) @@ -25,21 +43,19 @@ async def test_get_courses_with_announcements(database_session: AsyncSession): assert results[0] == course_1 -async def test_create_new_announcement(ufora_course: UforaCourse, database_session: AsyncSession): +async def test_create_new_announcement(course: UforaCourse, database_session: AsyncSession): """Test creating a new announcement""" - await crud.create_new_announcement( - database_session, 1, course=ufora_course, publication_date=datetime.datetime.now() - ) - await database_session.refresh(ufora_course) - assert len(ufora_course.announcements) == 1 + await crud.create_new_announcement(database_session, 1, course=course, publication_date=datetime.datetime.now()) + await database_session.refresh(course) + assert len(course.announcements) == 1 -async def test_remove_old_announcements(ufora_announcement: UforaAnnouncement, database_session: AsyncSession): +async def test_remove_old_announcements(announcement: UforaAnnouncement, database_session: AsyncSession): """Test removing all stale announcements""" - course = ufora_announcement.course - ufora_announcement.publication_date -= datetime.timedelta(weeks=2) - announcement_2 = UforaAnnouncement(course_id=ufora_announcement.course_id, publication_date=datetime.datetime.now()) - database_session.add_all([ufora_announcement, announcement_2]) + course = announcement.course + announcement.publication_date -= datetime.timedelta(weeks=2) + announcement_2 = UforaAnnouncement(course_id=announcement.course_id, publication_date=datetime.datetime.now()) + database_session.add_all([announcement, announcement_2]) await database_session.commit() await database_session.refresh(course) assert len(course.announcements) == 2 diff --git a/tests/test_database/test_crud/test_ufora_courses.py b/tests/test_database/test_crud/test_ufora_courses.py deleted file mode 100644 index d2d5e1b..0000000 --- a/tests/test_database/test_crud/test_ufora_courses.py +++ /dev/null @@ -1,22 +0,0 @@ -from sqlalchemy.ext.asyncio import AsyncSession - -from database.crud import ufora_courses as crud -from database.models import UforaCourse - - -async def test_get_course_by_name_exact(database_session: AsyncSession, ufora_course: UforaCourse): - """Test getting a course by its name when the query is an exact match""" - match = await crud.get_course_by_name(database_session, "Test") - assert match == ufora_course - - -async def test_get_course_by_name_substring(database_session: AsyncSession, ufora_course: UforaCourse): - """Test getting a course by its name when the query is a substring""" - match = await crud.get_course_by_name(database_session, "es") - assert match == ufora_course - - -async def test_get_course_by_name_alias(database_session: AsyncSession, ufora_course_with_alias: UforaCourse): - """Test getting a course by its name when the name doesn't match, but the alias does""" - match = await crud.get_course_by_name(database_session, "ali") - assert match == ufora_course_with_alias diff --git a/tests/test_database/test_utils/__init__.py b/tests/test_database/test_utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_database/test_utils/test_caches.py b/tests/test_database/test_utils/test_caches.py deleted file mode 100644 index 09583d3..0000000 --- a/tests/test_database/test_utils/test_caches.py +++ /dev/null @@ -1,27 +0,0 @@ -from sqlalchemy.ext.asyncio import AsyncSession - -from database.models import UforaCourse -from database.utils.caches import UforaCourseCache - - -async def test_ufora_course_cache_refresh_empty(database_session: AsyncSession, ufora_course_with_alias: UforaCourse): - """Test loading the data for the Ufora Course cache when it's empty""" - cache = UforaCourseCache() - await cache.refresh(database_session) - - assert len(cache.data) == 2 - assert cache.data == ["alias", "test"] - - -async def test_ufora_course_cache_refresh_not_empty( - database_session: AsyncSession, ufora_course_with_alias: UforaCourse -): - """Test loading the data for the Ufora Course cache when it's not empty anymore""" - cache = UforaCourseCache() - cache.data = ["Something"] - cache.data_transformed = ["something"] - - await cache.refresh(database_session) - - assert len(cache.data) == 2 - assert cache.data == ["alias", "test"]