diff --git a/didier/cogs/currency.py b/didier/cogs/currency.py index 05446f0..f7204a8 100644 --- a/didier/cogs/currency.py +++ b/didier/cogs/currency.py @@ -27,10 +27,13 @@ class Currency(commands.Cog): @commands.command(name="Award") @commands.check(is_owner) - async def award(self, ctx: commands.Context, user: discord.User, amount: abbreviated_number): # type: ignore + async def award( + self, + ctx: commands.Context, + user: discord.User, + amount: typing.Annotated[int, abbreviated_number], + ): """Award a user a given amount of Didier Dinks""" - amount = typing.cast(int, amount) - async with self.client.postgres_session as session: await crud.add_dinks(session, user.id, amount) plural = pluralize("Didier Dink", amount) @@ -45,8 +48,7 @@ class Currency(commands.Cog): async with self.client.postgres_session as session: bank = await crud.get_bank(session, ctx.author.id) - embed = discord.Embed(colour=discord.Colour.blue()) - embed.set_author(name=f"{ctx.author.display_name}'s Bank") + embed = discord.Embed(title=f"{ctx.author.display_name}'s Bank", colour=discord.Colour.blue()) embed.set_thumbnail(url=ctx.author.avatar.url) embed.add_field(name="Interest level", value=bank.interest_level) @@ -61,8 +63,7 @@ class Currency(commands.Cog): async with self.client.postgres_session as session: bank = await crud.get_bank(session, ctx.author.id) - embed = discord.Embed(colour=discord.Colour.blue()) - embed.set_author(name="Bank upgrades") + embed = discord.Embed(title="Bank upgrades", colour=discord.Colour.blue()) embed.add_field( name=f"Interest ({bank.interest_level})", value=str(interest_upgrade_price(bank.interest_level)) @@ -118,10 +119,8 @@ class Currency(commands.Cog): await ctx.reply(f"**{ctx.author.display_name}** has **{bank.dinks}** {plural}.", mention_author=False) @commands.command(name="Invest", aliases=["Deposit", "Dep"]) - async def invest(self, ctx: commands.Context, amount: abbreviated_number): # type: ignore + async def invest(self, ctx: commands.Context, amount: typing.Annotated[typing.Union[str, int], abbreviated_number]): """Invest a given amount of Didier Dinks""" - amount = typing.cast(typing.Union[str, int], amount) - async with self.client.postgres_session as session: invested = await crud.invest(session, ctx.author.id, amount) plural = pluralize("Didier Dink", invested) diff --git a/didier/cogs/help.py b/didier/cogs/help.py index 77ae0f1..73c66e5 100644 --- a/didier/cogs/help.py +++ b/didier/cogs/help.py @@ -10,13 +10,12 @@ 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, so we do some fiddling with it """ def _help_embed_base(self, title: str) -> discord.Embed: """Create the base structure for the embeds that get sent with the Help commands""" - embed = discord.Embed(colour=discord.Colour.blue()) - embed.set_author(name=title) + embed = discord.Embed(title=title, colour=discord.Colour.blue()) embed.set_footer(text="Syntax: Didier Help [Categorie] of Didier Help [Commando]") return embed diff --git a/didier/cogs/school.py b/didier/cogs/school.py index 460f2b2..f8a0d1b 100644 --- a/didier/cogs/school.py +++ b/didier/cogs/school.py @@ -1,3 +1,6 @@ +from datetime import date +from typing import Optional + import discord from discord import app_commands from discord.ext import commands @@ -5,7 +8,11 @@ from discord.ext import commands from database.crud import ufora_courses from database.crud.deadlines import get_deadlines from didier import Didier +from didier.data.apis.hydra import fetch_menu from didier.data.embeds.deadlines import Deadlines +from didier.data.embeds.hydra import no_menu_found +from didier.exceptions import HTTPException +from didier.utils.discord.converters.time import DateTransformer from didier.utils.discord.flags.school import StudyGuideFlags @@ -26,6 +33,25 @@ class School(commands.Cog): embed = Deadlines(deadlines).to_embed() await ctx.reply(embed=embed, mention_author=False, ephemeral=False) + @commands.hybrid_command( + name="menu", + description="Show the menu in the Ghent University restaurants.", + aliases=["Eten", "Food"], + ) + @app_commands.rename(day_dt="date") + async def menu(self, ctx: commands.Context, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None): + """Show the menu in the Ghent University restaurants. + + Menus are Dutch, as a lot of dishes have very weird translations + """ + async with ctx.typing(): + try: + menu = await fetch_menu(self.client.http_session, day_dt) + embed = menu.to_embed(day_dt=day_dt) + except HTTPException: + embed = no_menu_found(day_dt) + await ctx.reply(embed=embed, mention_author=False) + @commands.hybrid_command( name="fiche", description="Sends the link to the study guide for [Course]", aliases=["guide", "studiefiche"] ) diff --git a/didier/data/apis/hydra.py b/didier/data/apis/hydra.py new file mode 100644 index 0000000..620e0df --- /dev/null +++ b/didier/data/apis/hydra.py @@ -0,0 +1,15 @@ +from datetime import date + +from aiohttp import ClientSession + +from didier.data.embeds.hydra import Menu +from didier.utils.http.requests import ensure_get + +__all__ = ["fetch_menu"] + + +async def fetch_menu(http_session: ClientSession, day_dt: date) -> Menu: + """Fetch the menu for a given day""" + endpoint = f"https://hydra.ugent.be/api/2.0/resto/menu/nl/{day_dt.year}/{day_dt.month}/{day_dt.day}.json" + async with ensure_get(http_session, endpoint, log_exceptions=False) as response: + return Menu.parse_obj(response) diff --git a/didier/data/embeds/base.py b/didier/data/embeds/base.py index 45be7a0..4b32224 100644 --- a/didier/data/embeds/base.py +++ b/didier/data/embeds/base.py @@ -13,7 +13,7 @@ class EmbedBaseModel(ABC): """Abstract base class for a model that can be turned into a Discord embed""" @abstractmethod - def to_embed(self, **kwargs: dict) -> discord.Embed: + def to_embed(self, **kwargs) -> discord.Embed: """Turn this model into a Discord embed""" raise NotImplementedError diff --git a/didier/data/embeds/deadlines.py b/didier/data/embeds/deadlines.py index 6f46bbd..371eee9 100644 --- a/didier/data/embeds/deadlines.py +++ b/didier/data/embeds/deadlines.py @@ -22,9 +22,8 @@ class Deadlines(EmbedBaseModel): self.deadlines.sort(key=lambda deadline: deadline.deadline) @overrides - def to_embed(self, **kwargs: dict) -> discord.Embed: - embed = discord.Embed(colour=discord.Colour.dark_gold()) - embed.set_author(name="Upcoming Deadlines") + def to_embed(self, **kwargs) -> discord.Embed: + embed = discord.Embed(title="Upcoming Deadlines", colour=discord.Colour.dark_gold()) now = tz_aware_now() has_active_deadlines = False diff --git a/didier/data/embeds/error_embed.py b/didier/data/embeds/error_embed.py index 0dc1b80..9118c6d 100644 --- a/didier/data/embeds/error_embed.py +++ b/didier/data/embeds/error_embed.py @@ -37,8 +37,7 @@ def create_error_embed(ctx: commands.Context, exception: Exception) -> discord.E invocation = f"{ctx.author.display_name} in {origin}" - embed = discord.Embed(colour=discord.Colour.red()) - embed.set_author(name="Error") + embed = discord.Embed(title="Error", colour=discord.Colour.red()) embed.add_field(name="Command", value=f"{ctx.message.content}", inline=True) embed.add_field(name="Context", value=invocation, inline=True) embed.add_field(name="Exception", value=abbreviate(str(exception), Limits.EMBED_FIELD_VALUE_LENGTH), inline=False) diff --git a/didier/data/embeds/google/google_search.py b/didier/data/embeds/google/google_search.py index dcbad9c..dd0383b 100644 --- a/didier/data/embeds/google/google_search.py +++ b/didier/data/embeds/google/google_search.py @@ -19,8 +19,7 @@ class GoogleSearch(EmbedBaseModel): def _error_embed(self) -> discord.Embed: """Custom embed for unsuccessful requests""" - embed = discord.Embed(colour=discord.Colour.red()) - embed.set_author(name="Google Search") + embed = discord.Embed(title="Google Search", colour=discord.Colour.red()) # Empty embed if not self.data.results: @@ -33,12 +32,11 @@ class GoogleSearch(EmbedBaseModel): return embed @overrides - def to_embed(self, **kwargs: dict) -> discord.Embed: + def to_embed(self, **kwargs) -> discord.Embed: if not self.data.results or self.data.status_code != HTTPStatus.OK: return self._error_embed() - embed = discord.Embed(colour=discord.Colour.blue()) - embed.set_author(name="Google Search") + embed = discord.Embed(title="Google Search", colour=discord.Colour.blue()) embed.set_footer(text=self.data.result_stats or None) # Add all results into the description diff --git a/didier/data/embeds/hydra/__init__.py b/didier/data/embeds/hydra/__init__.py new file mode 100644 index 0000000..798cd46 --- /dev/null +++ b/didier/data/embeds/hydra/__init__.py @@ -0,0 +1,3 @@ +from .menu import Menu, no_menu_found + +__all__ = ["Menu", "no_menu_found"] diff --git a/didier/data/embeds/hydra/menu.py b/didier/data/embeds/hydra/menu.py new file mode 100644 index 0000000..8d1e8d4 --- /dev/null +++ b/didier/data/embeds/hydra/menu.py @@ -0,0 +1,126 @@ +from datetime import date +from enum import Enum +from typing import Optional, cast + +import discord +from overrides import overrides +from pydantic import BaseModel + +from didier.data.embeds.base import EmbedPydantic +from didier.utils.discord.colours import ghent_university_blue, ghent_university_yellow +from didier.utils.types.datetime import int_to_weekday +from didier.utils.types.string import leading + +__all__ = ["Menu", "no_menu_found"] + + +class _MealKind(str, Enum): + FISH = "fish" + MEAT = "meat" + SOUP = "soup" + VEGAN = "vegan" + VEGETARIAN = "vegetarian" + + +class _MealType(str, Enum): + COLD = "cold" + MAIN = "main" + SIDE = "side" + + +class _Meal(BaseModel): + """Model for an item on the menu""" + + kind: _MealKind + name: str + price: str + type: _MealType + + +class Menu(EmbedPydantic): + """Embed that shows the menu in Ghent University restaurants""" + + meals: list[_Meal] = [] + open: bool + vegetables: list[str] = [] + message: Optional[str] = None + + def _get_dutch_meal_prefix(self, meal: _Meal) -> str: + if meal.kind == _MealKind.MEAT: + prefix = "Vlees" + elif meal.kind == _MealKind.FISH: + prefix = "Vis" + elif meal.kind == _MealKind.VEGETARIAN: + prefix = "Vegetarisch" + else: + prefix = "Vegan" + + return prefix + + def _get_soups(self) -> str: + acc = "" + + for meal in self.meals: + if meal.kind == _MealKind.SOUP: + acc += f"{meal.name} ({meal.price})\n" + + return acc.strip() + + def _get_main_courses(self) -> str: + acc = "" + + for meal in self.meals: + if meal.type != _MealType.MAIN: + continue + + prefix = self._get_dutch_meal_prefix(meal) + + acc += f"* {prefix}: {meal.name} ({meal.price})\n" + + return acc.strip() + + def _get_cold_meals(self) -> str: + acc = "" + + for meal in self.meals: + if meal.type == _MealType.COLD: + acc += f"* {self._get_dutch_meal_prefix(meal)}: {meal.name} ({meal.price})\n" + + return acc.strip() + + def _closed_embed(self, embed: discord.Embed) -> discord.Embed: + embed.colour = ghent_university_yellow() + embed.description = "The restaurants are closed today." + return embed + + def _regular_embed(self, embed: discord.Embed) -> discord.Embed: + embed.add_field(name="🥣 Soep", value=self._get_soups(), inline=False) + embed.add_field(name="🍴 Hoofdgerechten", value=self._get_main_courses(), inline=False) + embed.add_field(name="❄️Koud", value=self._get_cold_meals(), inline=False) + + vegetables = "\n".join(list(sorted(self.vegetables))) + embed.add_field(name="🥦 Groenten", value=vegetables, inline=False) + + return embed + + @overrides + def to_embed(self, **kwargs) -> discord.Embed: + day_dt: date = cast(date, kwargs.get("day_dt")) + weekday = int_to_weekday(day_dt.weekday()) + formatted_date = f"{leading('0', str(day_dt.day))}/{leading('0', str(day_dt.month))}/{day_dt.year}" + + embed = discord.Embed(title=f"Menu - {weekday} {formatted_date}", colour=ghent_university_blue()) + + embed = self._regular_embed(embed) if self.open else self._closed_embed(embed) + + if self.message: + embed.add_field(name="📣 Extra Mededeling", value=self.message, inline=False) + + return embed + + +def no_menu_found(day_dt: date) -> discord.Embed: + """Return a different embed if no menu could be found""" + embed = discord.Embed(title="Menu", colour=discord.Colour.red()) + embed.description = f"Unable to retrieve menu for {day_dt.strftime('%d/%m/%Y')}." + return embed diff --git a/didier/data/embeds/ufora/announcements.py b/didier/data/embeds/ufora/announcements.py index a766e79..59e8290 100644 --- a/didier/data/embeds/ufora/announcements.py +++ b/didier/data/embeds/ufora/announcements.py @@ -15,6 +15,7 @@ import settings from database.crud import ufora_announcements as crud from database.schemas.relational import UforaCourse from didier.data.embeds.base import EmbedBaseModel +from didier.utils.discord.colours import ghent_university_blue from didier.utils.types.datetime import int_to_weekday from didier.utils.types.string import leading @@ -47,9 +48,9 @@ class UforaNotification(EmbedBaseModel): self.published_dt = self._published_datetime() self._published = self._get_published() - def to_embed(self, **kwargs: dict) -> discord.Embed: + def to_embed(self, **kwargs) -> discord.Embed: """Turn the notification into an embed""" - embed = discord.Embed(colour=discord.Colour.from_rgb(30, 100, 200)) + embed = discord.Embed(title=self._title, colour=ghent_university_blue()) embed.set_author(name=self.course.name) embed.title = self._title diff --git a/didier/data/embeds/urban_dictionary.py b/didier/data/embeds/urban_dictionary.py index afaf4c5..3126fed 100644 --- a/didier/data/embeds/urban_dictionary.py +++ b/didier/data/embeds/urban_dictionary.py @@ -46,9 +46,8 @@ class Definition(EmbedPydantic): return string_utils.abbreviate(field, max_length=Limits.EMBED_FIELD_VALUE_LENGTH) @overrides - def to_embed(self, **kwargs: dict) -> discord.Embed: - embed = discord.Embed(colour=colours.urban_dictionary_green()) - embed.set_author(name="Urban Dictionary") + def to_embed(self, **kwargs) -> discord.Embed: + embed = discord.Embed(title="Urban Dictionary", colour=colours.urban_dictionary_green()) embed.add_field(name="Term", value=self.word, inline=True) embed.add_field(name="Author", value=self.author, inline=True) diff --git a/didier/data/embeds/wordle.py b/didier/data/embeds/wordle.py index 44439c0..d29a29f 100644 --- a/didier/data/embeds/wordle.py +++ b/didier/data/embeds/wordle.py @@ -126,7 +126,7 @@ class WordleErrorEmbed(EmbedBaseModel): message: str @overrides - def to_embed(self, **kwargs: dict) -> discord.Embed: + def to_embed(self, **kwargs) -> discord.Embed: embed = discord.Embed(colour=discord.Colour.red(), title="Wordle") embed.description = self.message embed.set_footer(text=footer()) diff --git a/didier/utils/discord/autocompletion/__init__.py b/didier/utils/discord/autocompletion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/didier/utils/discord/autocompletion/time.py b/didier/utils/discord/autocompletion/time.py new file mode 100644 index 0000000..b5003fd --- /dev/null +++ b/didier/utils/discord/autocompletion/time.py @@ -0,0 +1,31 @@ +from discord import app_commands + +__all__ = ["autocomplete_day"] + + +def autocomplete_day(argument: str) -> list[app_commands.Choice]: + """Autocompletion for day-arguments + + This supports relative offsets ("tomorrow") as well as weekdays + """ + argument = argument.lower() + values = [ + "Tomorrow", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Sunday", + "Morgen", + "Overmorgen", + "Maandag", + "Dinsdag", + "Woensdag", + "Donderdag", + "Vrijdag", + "Zaterdag", + "Zondag", + ] + + return [app_commands.Choice(name=value, value=value.lower()) for value in values if argument in value.lower()] diff --git a/didier/utils/discord/colours.py b/didier/utils/discord/colours.py index ffcd8eb..7c26769 100644 --- a/didier/utils/discord/colours.py +++ b/didier/utils/discord/colours.py @@ -1,6 +1,14 @@ import discord -__all__ = ["urban_dictionary_green"] +__all__ = ["ghent_university_blue", "ghent_university_yellow", "urban_dictionary_green"] + + +def ghent_university_blue() -> discord.Colour: + return discord.Colour.from_rgb(30, 100, 200) + + +def ghent_university_yellow() -> discord.Colour: + return discord.Colour.from_rgb(255, 210, 0) def urban_dictionary_green() -> discord.Colour: diff --git a/didier/utils/discord/converters/time.py b/didier/utils/discord/converters/time.py new file mode 100644 index 0000000..6fbad9a --- /dev/null +++ b/didier/utils/discord/converters/time.py @@ -0,0 +1,66 @@ +import contextlib +import datetime +from datetime import date, timedelta +from typing import Optional, Union + +import discord +from discord import app_commands +from discord.ext.commands import ArgumentParsingError +from overrides import overrides + +from didier.utils.discord.autocompletion.time import autocomplete_day +from didier.utils.types.datetime import ( + forward_to_next_weekday, + parse_dm_string, + str_to_weekday, +) + +__all__ = ["date_converter"] + + +def date_converter(argument: Optional[str]) -> date: + """Converter to turn a string into a date""" + # Store original argument for error message purposes + original_argument = argument + + # Default to today + if not argument: + return date.today() + + argument = argument.lower() + + # Manual offsets + if argument in ( + "tomorrow", + "tmrw", + "morgen", + ): + return date.today() + timedelta(days=1) + + if argument in ("overmorgen",): + return date.today() + timedelta(days=2) + + # Weekdays passed in words + with contextlib.suppress(ValueError): + weekday = str_to_weekday(argument) + return forward_to_next_weekday(date.today(), weekday, allow_today=False) + + # Date strings + with contextlib.suppress(ValueError): + return parse_dm_string(argument) + + # Unparseable + raise ArgumentParsingError(f"Unable to interpret `{original_argument}` as a date.") + + +class DateTransformer(app_commands.Transformer): + """Application commands transformer for dates""" + + async def autocomplete( + self, interaction: discord.Interaction, value: Union[int, float, str] + ) -> list[app_commands.Choice[Union[int, float, str]]]: + return autocomplete_day(str(value)) + + @overrides + async def transform(self, interaction: discord.Interaction, value: str) -> datetime.date: + return date_converter(value) diff --git a/didier/utils/http/requests.py b/didier/utils/http/requests.py index f649701..ffcb498 100644 --- a/didier/utils/http/requests.py +++ b/didier/utils/http/requests.py @@ -2,7 +2,7 @@ import logging from contextlib import asynccontextmanager from typing import AsyncGenerator -from aiohttp import ClientResponse, ClientSession +from aiohttp import ClientResponse, ClientSession, ContentTypeError from didier.exceptions.http_exception import HTTPException @@ -18,13 +18,19 @@ def request_successful(response: ClientResponse) -> bool: @asynccontextmanager -async def ensure_get(http_session: ClientSession, endpoint: str) -> AsyncGenerator[dict, None]: +async def ensure_get( + http_session: ClientSession, endpoint: str, *, log_exceptions: bool = True +) -> AsyncGenerator[dict, None]: """Context manager that automatically raises an exception if a GET-request fails""" async with http_session.get(endpoint) as response: + try: + content = await response.json() + except ContentTypeError: + content = await response.text() + if not request_successful(response): - logger.error( - "Failed HTTP request to %s (status %s)\nResponse: %s", endpoint, response.status, await response.json() - ) + if log_exceptions: + logger.error("Failed HTTP request to %s (status %s)\nResponse: %s", endpoint, response.status, content) raise HTTPException(response.status) @@ -33,18 +39,29 @@ async def ensure_get(http_session: ClientSession, endpoint: str) -> AsyncGenerat @asynccontextmanager async def ensure_post( - http_session: ClientSession, endpoint: str, payload: dict, *, expect_return: bool = True + http_session: ClientSession, + endpoint: str, + payload: dict, + *, + log_exceptions: bool = True, + expect_return: bool = True ) -> AsyncGenerator[dict, None]: """Context manager that automatically raises an exception if a POST-request fails""" async with http_session.post(endpoint, data=payload) as response: if not request_successful(response): - logger.error( - "Failed HTTP request to %s (status %s)\nPayload: %s\nResponse: %s", - endpoint, - response.status, - payload, - await response.json(), - ) + try: + content = await response.json() + except ContentTypeError: + content = await response.text() + + if log_exceptions: + logger.error( + "Failed HTTP request to %s (status %s)\nPayload: %s\nResponse: %s", + endpoint, + response.status, + payload, + content, + ) raise HTTPException(response.status) diff --git a/didier/utils/types/datetime.py b/didier/utils/types/datetime.py index 7b2d5c1..07ad29a 100644 --- a/didier/utils/types/datetime.py +++ b/didier/utils/types/datetime.py @@ -1,18 +1,91 @@ import datetime +import re import zoneinfo +from typing import TypeVar, Union -__all__ = ["LOCAL_TIMEZONE", "int_to_weekday", "str_to_date", "tz_aware_now"] +__all__ = [ + "LOCAL_TIMEZONE", + "forward_to_next_weekday", + "int_to_weekday", + "parse_dm_string", + "str_to_date", + "str_to_month", + "str_to_weekday", + "tz_aware_now", +] -from typing import Union +DateType = TypeVar("DateType", datetime.date, datetime.datetime) LOCAL_TIMEZONE = zoneinfo.ZoneInfo("Europe/Brussels") +def forward_to_next_weekday(day_dt: DateType, target_weekday: int, *, allow_today: bool = False) -> DateType: + """Forward a date to the next occurence of a weekday""" + if not 0 <= target_weekday <= 6: + raise ValueError + + # Skip at least one day + if not allow_today: + day_dt += datetime.timedelta(days=1) + + while day_dt.weekday() != target_weekday: + day_dt += datetime.timedelta(days=1) + + return day_dt + + def int_to_weekday(number: int) -> str: # pragma: no cover # it's useless to write a test for this """Get the Dutch name of a weekday from the number""" return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][number] +def parse_dm_string(argument: str) -> datetime.date: + """Parse a string to [day]/[month] + + The year is set to the current year by default, as this can be changed easily. + + This supports: + - DD/MM + - DD (month defaults to current) + - DD [Dutch Month, possibly abbreviated] + - DD [English Month, possibly abbreviated] + - [Dutch Month, possibly abbreviated] DD + - [English Month, possibly abbreviated] DD + """ + argument = argument.lower() + today = datetime.date.today() + + # DD/MM + if "/" in argument: + spl = argument.split("/") + if len(spl) != 2: + raise ValueError + + return datetime.date(day=int(spl[0]), month=int(spl[1]), year=today.year) + + # Try to interpret text + spl = argument.split(" ") + if len(spl) != 2: + raise ValueError + + # Day Month + match = re.search(r"\d+", spl[0]).group() + if match is not None: + day = int(match) + month = str_to_month(spl[1]) + return datetime.date(day=day, month=month, year=today.year) + + # Month Day + match = re.search(r"\d+", spl[0]).group() + if match is not None: + day = int(match) + month = str_to_month(spl[0]) + return datetime.date(day=day, month=month, year=today.year) + + # Unparseable + raise ValueError + + def str_to_date(date_str: str, formats: Union[list[str], str] = "%d/%m/%Y") -> datetime.date: """Turn a string into a DD/MM/YYYY date""" # Allow passing multiple formats in a list @@ -28,6 +101,76 @@ def str_to_date(date_str: str, formats: Union[list[str], str] = "%d/%m/%Y") -> d raise ValueError +def str_to_month(argument: str) -> int: + """Turn a string int oa month, bilingual""" + argument = argument.lower() + + month_dict = { + # English + "january": 1, + "february": 2, + "march": 3, + "april": 4, + "may": 5, + "june": 6, + "july": 7, + # August is a prefix of Augustus so it is skipped + "september": 9, + "october": 10, + "november": 11, + "december": 12, + # Dutch + "januari": 1, + "februari": 2, + "maart": 3, + # April is the same in English so it is skipped + "mei": 5, + "juni": 6, + "juli": 7, + "augustus": 8, + # September is the same in English so it is skipped + "oktober": 10, + # November is the same in English so it is skipped + # December is the same in English so it is skipped + } + + for key, value in month_dict.items(): + if key.startswith(argument): + return value + + raise ValueError + + +def str_to_weekday(argument: str) -> int: + """Turn a string into a weekday, bilingual""" + argument = argument.lower() + + weekday_dict = { + # English + "monday": 0, + "tuesday": 1, + "wednesday": 2, + "thursday": 3, + "friday": 4, + "saturday": 5, + "sunday": 6, + # Dutch + "maandag": 0, + "dinsdag": 1, + "woensdag": 2, + "donderdag": 3, + "vrijdag": 4, + "zaterdag": 5, + "zondag": 6, + } + + for key, value in weekday_dict.items(): + if key.startswith(argument): + return value + + raise ValueError + + def tz_aware_now() -> datetime.datetime: """Get the current date & time, but timezone-aware""" return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone(LOCAL_TIMEZONE)