mirror of https://github.com/stijndcl/didier
Add flake8 docstring plugin, formatting, create base class for embeds & models
parent
8d6dbe1c94
commit
b9c5c6ab10
25
.flake8
25
.flake8
|
@ -1,4 +1,5 @@
|
|||
[flake8]
|
||||
# Don't lint non-Python files
|
||||
exclude =
|
||||
.git,
|
||||
.github,
|
||||
|
@ -9,10 +10,30 @@ exclude =
|
|||
htmlcov,
|
||||
tests,
|
||||
venv
|
||||
ignore=E203
|
||||
# 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
|
||||
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
|
||||
./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
|
||||
|
|
|
@ -28,6 +28,7 @@ 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.
|
||||
"""
|
||||
|
|
|
@ -12,7 +12,8 @@ __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()
|
||||
|
|
|
@ -2,13 +2,15 @@ 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
|
||||
|
||||
The default is ugly as hell so we do some fiddling with it
|
||||
"""
|
||||
|
||||
def _help_embed_base(self, title: str) -> discord.Embed:
|
||||
|
@ -30,6 +32,7 @@ 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()))
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
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.command(name="Define", 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 = urban_dictionary.lookup(self.client.http_session, query)
|
||||
|
||||
|
||||
async def setup(client: Didier):
|
||||
"""Load the cog"""
|
||||
await client.add_cog(Other(client))
|
|
@ -25,8 +25,9 @@ class Owner(commands.Cog):
|
|||
self.client = client
|
||||
|
||||
async def cog_check(self, ctx: commands.Context) -> bool:
|
||||
"""Global check for every command in this cog, so we don't have to add
|
||||
is_owner() to every single command separately
|
||||
"""Global check for every command in this cog
|
||||
|
||||
This means that 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)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from aiohttp import ClientSession
|
||||
|
||||
from didier.data.embeds.urban_dictionary import Definition
|
||||
|
||||
__all__ = ["lookup"]
|
||||
|
||||
|
||||
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"]))
|
|
@ -0,0 +1,22 @@
|
|||
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(ABC, EmbedBaseModel, BaseModel):
|
||||
"""Pydantic version of EmbedModel"""
|
|
@ -14,6 +14,7 @@ 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
|
||||
|
||||
|
@ -25,7 +26,7 @@ __all__ = [
|
|||
|
||||
|
||||
@dataclass
|
||||
class UforaNotification:
|
||||
class UforaNotification(EmbedBaseModel):
|
||||
"""A single notification from Ufora"""
|
||||
|
||||
content: dict
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from overrides import overrides
|
||||
|
||||
from didier.data.embeds.base import EmbedPydantic
|
||||
|
||||
__all__ = ["Definition"]
|
||||
|
||||
|
||||
class Definition(EmbedPydantic):
|
||||
"""A definition from the Urban Dictionary"""
|
||||
|
||||
word: str
|
||||
definition: str
|
||||
permalink: str
|
||||
author: str
|
||||
thumbs_up: int
|
||||
thumbs_down: int
|
||||
written_on: datetime
|
||||
|
||||
@overrides
|
||||
def to_embed(self) -> discord.Embed:
|
||||
embed = discord.Embed()
|
|
@ -2,6 +2,7 @@ import traceback
|
|||
import typing
|
||||
|
||||
import discord
|
||||
from overrides import overrides
|
||||
|
||||
from database.crud.custom_commands import create_command, edit_command
|
||||
from didier import Didier
|
||||
|
@ -24,12 +25,14 @@ 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__)
|
||||
|
@ -37,7 +40,8 @@ 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
|
||||
|
||||
Fills in the current values as defaults for QOL
|
||||
"""
|
||||
|
||||
name: discord.ui.TextInput
|
||||
|
@ -59,6 +63,7 @@ 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])
|
||||
|
@ -68,6 +73,7 @@ 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__)
|
||||
|
|
|
@ -42,7 +42,10 @@ class Didier(commands.Bot):
|
|||
return DBSession()
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
"""Hook called once the bot is initialised"""
|
||||
"""Do some initial setup
|
||||
|
||||
This hook is called once the bot is initialised
|
||||
"""
|
||||
# Load extensions
|
||||
await self._load_initial_extensions()
|
||||
await self._load_directory_extensions("didier/cogs")
|
||||
|
@ -113,7 +116,9 @@ 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):
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
__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}")
|
|
@ -0,0 +1,7 @@
|
|||
import discord
|
||||
|
||||
__all__ = ["urban_dictionary_green"]
|
||||
|
||||
|
||||
def urban_dictionary_green() -> discord.Colour:
|
||||
return discord.Colour.from_rgb(220, 255, 0)
|
|
@ -6,6 +6,7 @@ __all__ = ["abbreviated_number"]
|
|||
|
||||
def abbreviated_number(argument: str) -> Union[str, int]:
|
||||
"""Custom converter to allow numbers to be abbreviated
|
||||
|
||||
Examples:
|
||||
515k
|
||||
4m
|
||||
|
|
|
@ -10,6 +10,7 @@ __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.
|
||||
"""
|
||||
|
|
|
@ -6,6 +6,7 @@ __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
|
||||
|
|
|
@ -22,6 +22,7 @@ profile = "black"
|
|||
|
||||
[tool.mypy]
|
||||
plugins = [
|
||||
"pydantic.mypy",
|
||||
"sqlalchemy.ext.mypy.plugin"
|
||||
]
|
||||
[[tool.mypy.overrides]]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,5 +6,8 @@ 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
|
||||
|
|
33
settings.py
33
settings.py
|
@ -6,6 +6,26 @@ 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")
|
||||
|
@ -18,13 +38,14 @@ DB_HOST: str = env.str("DB_HOST", "localhost")
|
|||
DB_PORT: int = env.int("DB_PORT", "5432")
|
||||
|
||||
"""Discord"""
|
||||
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", "?")
|
||||
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", "?")
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"list": [
|
||||
{
|
||||
"definition": "When you fall [asleep] [tweeting] about [nonsensical] things",
|
||||
"permalink": "http://cofveve.urbanup.com/11642742",
|
||||
"thumbs_up": 170,
|
||||
"sound_urls": [],
|
||||
"author": "Bonafidé",
|
||||
"word": "cofveve",
|
||||
"defid": 11642742,
|
||||
"current_vote": "",
|
||||
"written_on": "2017-06-03T00:32:10.987Z",
|
||||
"example": "[Despite] [the negative] [press] cofveve",
|
||||
"thumbs_down": 30
|
||||
},
|
||||
{
|
||||
"definition": "when you want to type [conference] and your hands are too small to reach [the keys] (someone else's [brill] def)",
|
||||
"permalink": "http://cofveve.urbanup.com/11662158",
|
||||
"thumbs_up": 69,
|
||||
"sound_urls": [],
|
||||
"author": "blacklist2017",
|
||||
"word": "cofveve",
|
||||
"defid": 11662158,
|
||||
"current_vote": "",
|
||||
"written_on": "2017-06-07T00:44:36.793Z",
|
||||
"example": "\"[i want] to [shut down] this [press] cofveve...",
|
||||
"thumbs_down": 16
|
||||
},
|
||||
{
|
||||
"definition": "[Bullshit]",
|
||||
"permalink": "http://cofveve.urbanup.com/12386593",
|
||||
"thumbs_up": 5,
|
||||
"sound_urls": [],
|
||||
"author": "FreedomTodd",
|
||||
"word": "Cofveve",
|
||||
"defid": 12386593,
|
||||
"current_vote": "",
|
||||
"written_on": "2018-01-06T17:52:59.822Z",
|
||||
"example": "[I’m] [full of] [cofveve]",
|
||||
"thumbs_down": 6
|
||||
},
|
||||
{
|
||||
"definition": "When you dip [an apple] in a chocolate [Starbucks] [frappe]",
|
||||
"permalink": "http://razzle-cofveve.urbanup.com/12006856",
|
||||
"thumbs_up": 0,
|
||||
"sound_urls": [],
|
||||
"author": "harry potter theme song si ",
|
||||
"word": "razzle cofveve",
|
||||
"defid": 12006856,
|
||||
"current_vote": "",
|
||||
"written_on": "2017-09-30T03:00:54.233Z",
|
||||
"example": "[My friend] loves [apples] and [Starbucks], so the razzle cofveve was perfect!",
|
||||
"thumbs_down": 0
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue