From ed067489844bd96a5aabccf91f22a56730e03128 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Wed, 22 May 2024 16:12:20 -0400 Subject: [PATCH] Fix all smells in utils. --- README.md | 3 +- pyproject.toml | 1 + utils.py | 74 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index b55ea19..486876f 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,5 @@ Dakiny clan's own discord bot! ### Assumptions - For Elections: Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined. Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already -exist in the database. \ No newline at end of file +exist in the database. +- Bot is running on machine that is usint UTC time. All today() calls specify UTC time zone. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c1524b2..2d78ccb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ pandas = "1.5.2" python-dotenv = "1.0.0" pytz = "2022.7.1" sqlalchemy = "1.4.46" +aiofiles = "^23.2.1" [build-system] diff --git a/utils.py b/utils.py index a0f5aa5..1f903ad 100644 --- a/utils.py +++ b/utils.py @@ -1,12 +1,13 @@ -""" -File contains util functions. -""" +"""File contains util functions.""" +import calendar +import datetime import os from collections.abc import Sequence -from datetime import datetime from enum import StrEnum +from pathlib import Path +import aiofiles import nextcord from sqlalchemy import select, true, update from sqlalchemy.orm import Session @@ -29,7 +30,7 @@ TEMPLATES_DIR = f"{REPO_DIR}/message_templates" VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"]) -def get_datetime(day: str) -> datetime: +def get_datetime(day: str) -> datetime.datetime: """ Takes in a string month and day and returns a datetime object of that Month/Day this year. @@ -39,23 +40,24 @@ def get_datetime(day: str) -> datetime: Returns: datetime: Datetime representation of Month/Day. """ - year = datetime.today().year + today = datetime.datetime.now(tz=datetime.UTC).date() + year = today.year month = int(day.split("/")[0]) day = int(day.split("/")[1]) # ensure that year rolls over if current month is December and the next month is less than 12. - if datetime.today().month == 12 and month < 12: + if today.month == calendar.DECEMBER and month < calendar.DECEMBER: year += 1 - if datetime.today().month == 1 and month > 1: + if today.month == calendar.JANUARY and month > calendar.JANUARY: year -= 1 - return datetime(year, month, day) + return datetime.datetime(year, month, day, tzinfo=datetime.UTC) def get_election_name(config: DakinyConfig) -> str: """ - Builds the unique name of the current election cycle + Builds the unique name of the current election cycle. Args: config (DakinyConfig): the config used to know the election cycle. @@ -70,7 +72,7 @@ def get_election_name(config: DakinyConfig) -> str: def get_quarter_name(config: DakinyConfig) -> str: """ - Builds the name of the current cycle/quarter + Builds the name of the current cycle/quarter. Args: config (DakinyConfig): the config used to know the election cycle. @@ -94,7 +96,7 @@ def get_current_election_cycle(config: DakinyConfig) -> int: Returns: int: cycle index for active election. None for no active cycle """ - today = datetime.today() + today = datetime.datetime.now(tz=datetime.UTC).date() for cycle, election in enumerate(config.election_cycles): if ( @@ -109,7 +111,7 @@ def get_current_election_cycle(config: DakinyConfig) -> int: def get_quarter_cycle(config: DakinyConfig) -> int: """ - Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none + Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none. Args: config (DakinyConfig): Config object to use @@ -117,7 +119,7 @@ def get_quarter_cycle(config: DakinyConfig) -> int: Returns: int: cycle index for current cycle. """ - today = datetime.today() + today = datetime.datetime.now(tz=datetime.UTC).date() for cycle, election in enumerate(config.election_cycles): if ( today.date() >= get_datetime(election.term_start).date() @@ -170,10 +172,10 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> Args: config (DakinyConfig): Daking config object + guild (nextcord.Guild): The guild object. Used to get display names for sorting. Returns: str: String that contains all nominees and their votes. - guild (nextcord.Guild): The guild object. Used to get display names for sorting. """ cl_votes: dict[int, int] = {} dm_votes: dict[int, int] = {} @@ -298,16 +300,18 @@ def decorate_dm_nomination(user_id: int, speech: str) -> str: return msg -def record_event_points(config: DakinyConfig, users: Sequence[str], is_member: bool, add_points: int): +def record_event_points(config: DakinyConfig, users: Sequence[str], is_member: bool, add_points: int) -> None: # noqa: FBT001 """ - Adds event points to all users listed. Checks if they don't exist in order to make a new entry, otherwise + Adds event points to all users listed. + + Checks if they don't exist in order to make a new entry, otherwise adds points to the existing value. Can be used for hosting or participating. Args: config (DakinyConfig): Dakiny config for getting points users (Sequence[int]): users that participated and need points. is_member (bool): whether the users are a discord member or not - points (int): number of points to add + add_points (int): number of points to add """ quarter_name = get_quarter_name(config) with Session(engine) as session: @@ -337,7 +341,9 @@ def record_event_points(config: DakinyConfig, users: Sequence[str], is_member: b def get_allowed_points(session: Session, points_limit: int, user: str, points: int) -> int: """ - Gets the maximum number of points a user can be given. Searches how many points they have been given + Gets the maximum number of points a user can be given. + + Searches how many points they have been given this week and subtracts that from the weekly limit. If under, returns up to "points" number of points (will not go over points_limit) otherwise returns 0. Updates the limit object or creates a new one if one isn't found. @@ -351,7 +357,8 @@ def get_allowed_points(session: Session, points_limit: int, user: str, points: i int: how many points are allowed to be given to the user. """ # check for limit - this_week = f"{datetime.today().isocalendar().year}-{datetime.today().isocalendar().week}" + today = datetime.datetime.now(tz=datetime.UTC).date() + this_week = f"{today.isocalendar().year}-{today.isocalendar().week}" stmt = select(EventPointsLimit).where(EventPointsLimit.UserId == user).where(EventPointsLimit.Week == this_week) # if under limit, can add up to however many points would make the current limit equal to the @@ -374,12 +381,13 @@ def get_allowed_points(session: Session, points_limit: int, user: str, points: i return add_points -async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[str]): +async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[str]) -> None: """ Updates all clan messages to contents in MESSAGES_DIR. + Does not to any git work. Is expected to be called after git is pulled. - for ever directory, sort the files by number then edits/sends the message. + For every directory, sort the files by number then edits/sends the message. Channel fetching/management is handled by another function. Message editing and sending is handled by another function. @@ -388,7 +396,7 @@ async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[ guild (nextcord.Guild): Guild object that all channels and messages belong to. channel_updates: (Sequence[str]): a sequence of the channels to update. """ - dirs = [dir_name for dir_name in channel_updates if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}")] + dirs = [dir_name for dir_name in channel_updates if Path.is_dir(f"{MESSAGES_DIR}/{dir_name}")] with Session(engine) as session: for dir_name in dirs: @@ -402,13 +410,15 @@ async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[ for file_name in sorted_files: message_name = f"{dir_name}/{file_name}" file_path = f"{dir_path}/{file_name}" - with open(file_path, encoding="utf8") as reader: + with aiofiles.open(file_path, encoding="utf8") as reader: await edit_clan_message(session, channel, message_name, reader.read(2000)) async def get_clan_message_channel(session: Session, channel_name: str, guild: nextcord.Guild) -> nextcord.TextChannel: """ - Used to obtain a channel from the guild object. Searches database for an id to get the channel. If that is not + Used to obtain a channel from the guild object. + + Searches database for an id to get the channel. If that is not found, then searches through all channels in the guild to find one with a matching name. If that also doesn't turn up a channel, then creates it. Adds an entry into database for last two paths. Assumes that there is an open session to the database. @@ -463,9 +473,11 @@ async def edit_clan_message( channel: nextcord.TextChannel, message_name: str, message_content: str, -): +) -> None: """ - Obtains a message id from database. If no id, then need to send a new message and record the id. + Obtains a message id from database. + + If no id, then need to send a new message and record the id. If there is a message, then edit the message. If that message is not found in discord, then need to send a new message and record the id in the database. @@ -494,7 +506,7 @@ async def edit_clan_message( session.commit() -async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig): +async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) -> None: """ Gets the winners of the elections and updates the perminant message that states the current council. @@ -524,11 +536,13 @@ async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig): .where(CouncilMembers.Elected == true()) ) - council_members_strs = [f"<@{id}>" for id in session.scalars(stmt) if id not in (discord_manager, clan_leader)] + council_members_strs = [ + f"<@{user_id}>" for user_id in session.scalars(stmt) if user_id not in (discord_manager, clan_leader) + ] quarter = get_quarter_name(config) - with open(f"{TEMPLATES_DIR}/council_members.txt", encoding="utf8") as file: + with aiofiles.open(f"{TEMPLATES_DIR}/council_members.txt", encoding="utf8") as file: message = file.read() message = message.format( quarter=quarter,