Compare commits

..
Author SHA1 Message Date
imcompiling 09958c31ad Save: adding all files to save progress.
build_develop / build (push) Successful in 38s
I don't exactly remember where I was on this, but I need to move on
and work on other things. Adding all files just to save my work.
2025-07-15 09:12:38 -04:00
imcompiling 0f3596692f Move get_quarter_cycle into election_utils.py. 2025-04-24 09:34:53 -04:00
imcompiling def9c4aa79 Move get_datetime into own file.
Add test cases to cover get_datetime usage and edge cases.
2025-04-24 09:27:05 -04:00
imcompiling af3b8e9e69 Add lint ignores for tests.
build_develop / build (push) Successful in 44s
2025-04-24 08:59:47 -04:00
imcompiling a5655c4d6b Add a test for get_datetime. 2025-04-24 08:59:20 -04:00
imcompiling a277566654 Merge commit '58c167bbe30fa422277bef9e1ad0aed7bc350fc4' into missed-election 2025-04-23 14:54:01 -04:00
imcompiling 6adb98111b Add the freezegun module for testing. 2025-04-23 14:22:29 -04:00
imcompiling 4d96aebeb1 Address some pylance warnings. 2025-04-23 14:15:39 -04:00
imcompiling d38f56ff41 Rename module to conform to naming conventions. 2025-04-23 14:07:46 -04:00
imcompiling 95fb9e289b Imports got sorted, address some pylance warnings. 2025-04-23 14:05:36 -04:00
imcompiling 3bcd1a3a01 Update settings to handle lists
build_develop / build (push) Successful in 34s
lists won't ever happen, but pylance didn't like it.
2025-04-23 14:03:50 -04:00
imcompiling 6c57644eb0 Add pytest and coverage.
build_develop / build (push) Successful in 1m11s
2025-04-23 13:22:41 -04:00
27 changed files with 603 additions and 552 deletions
+1 -4
View File
@@ -9,10 +9,7 @@
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "dakiny_bot", "module": "dakiny_bot",
"justMyCode": true, "justMyCode": true
"env": {
"DEBUG": "true"
}
} }
] ]
} }
+2 -2
View File
@@ -1,10 +1,10 @@
FROM python:3.13.7-slim FROM python:3.13.3-slim
# install dependencies # install dependencies
RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3 RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3
# install uv # install uv
COPY --from=ghcr.io/astral-sh/uv:0.9.28 /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /bin/
COPY . /home COPY . /home
+6
View File
@@ -1 +1,7 @@
"""Empty init file for dakiny_bot module.""" """Empty init file for dakiny_bot module."""
from dakiny_bot.dakiny_settings import CONFIG_LOCATION, DakinySettings, settings
if settings is None:
tmp = DakinySettings.from_yaml_file(CONFIG_LOCATION)
settings = tmp[0] if isinstance(tmp, list) else tmp
+8 -1
View File
@@ -1,5 +1,6 @@
"""Main file for bot.""" """Main file for bot."""
import logging
import os import os
import nextcord import nextcord
@@ -11,10 +12,16 @@ from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView
from dakiny_bot.classes.voting_views import VoteView from dakiny_bot.classes.voting_views import VoteView
from dakiny_bot.database import create_database from dakiny_bot.database import create_database
from .DakinySettings import settings from .dakiny_settings import settings
birthday_list: list[int] = [] birthday_list: list[int] = []
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logger.addHandler(handler)
def run() -> None: def run() -> None:
"""Builds and runs bot.""" """Builds and runs bot."""
+6 -16
View File
@@ -9,21 +9,20 @@ from dakiny_bot.classes.nominate_highcouncil_views import NominateHighCouncilVie
from dakiny_bot.classes.nominate_views import NominateView from dakiny_bot.classes.nominate_views import NominateView
from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView from dakiny_bot.classes.voting_highcouncil_views import VoteHighcouncilView
from dakiny_bot.classes.voting_views import VoteView from dakiny_bot.classes.voting_views import VoteView
from dakiny_bot.DakinySettings import settings from dakiny_bot.dakiny_settings import settings
from dakiny_bot.database.sql_data_classes import ( from dakiny_bot.database.sql_data_classes import (
CouncilMembers, CouncilMembers,
ElectionInformation, ElectionInformation,
Votes, Votes,
engine, engine,
) )
from dakiny_bot.logger import dakiny_logger from dakiny_bot.election_utils import get_datetime
from dakiny_bot.utils import ( from dakiny_bot.utils import (
TEMPLATES_DIR, TEMPLATES_DIR,
VoteType, VoteType,
build_highcouncil_votes_str, build_highcouncil_votes_str,
build_votes_str, build_votes_str,
edit_clan_council_info, edit_clan_council_info,
get_datetime,
get_election_name, get_election_name,
) )
from sqlalchemy import select, true, update from sqlalchemy import select, true, update
@@ -70,7 +69,7 @@ class Election:
list[str]: A list of message contents in the correct order. Must be a string because the message is too list[str]: A list of message contents in the correct order. Must be a string because the message is too
long. long.
""" """
with Path(f"{TEMPLATES_DIR}/election_announcement.txt", encoding="utf8").open() as file: with Path(f"{TEMPLATES_DIR}/election_announcement.txt").open() as file:
message = file.read() message = file.read()
next_election = get_datetime( next_election = get_datetime(
@@ -98,7 +97,7 @@ class Election:
Returns: Returns:
str: Message to send. str: Message to send.
""" """
with Path(f"{TEMPLATES_DIR}/highcouncil_election.txt", encoding="utf8").open() as file: with Path(f"{TEMPLATES_DIR}/highcouncil_election.txt").open() as file:
message = file.read() message = file.read()
return message.format( return message.format(
@@ -116,7 +115,7 @@ class Election:
Returns: Returns:
str: Message to send. str: Message to send.
""" """
with Path(f"{TEMPLATES_DIR}/highcouncil_vote.txt", encoding="utf8").open() as file: with Path(f"{TEMPLATES_DIR}/highcouncil_vote.txt").open() as file:
message = file.read() message = file.read()
return message.format( return message.format(
@@ -131,7 +130,7 @@ class Election:
Returns: Returns:
str: Message to send str: Message to send
""" """
with Path(f"{TEMPLATES_DIR}/vote_start.txt", encoding="utf8").open() as file: with Path(f"{TEMPLATES_DIR}/vote_start.txt").open() as file:
message = file.read() message = file.read()
vote_end: datetime = get_datetime(settings.election_cycles[self.cycle].voting_end) vote_end: datetime = get_datetime(settings.election_cycles[self.cycle].voting_end)
@@ -144,29 +143,20 @@ class Election:
async def check_date(self) -> None: async def check_date(self) -> None:
"""Function checks current date to see if something needs to be run.""" """Function checks current date to see if something needs to be run."""
today = datetime.now(tz=UTC) today = datetime.now(tz=UTC)
dakiny_logger.debug("Checking date.")
# check all dates if action is needed # check all dates if action is needed
if today.date() == self.election_start.date(): if today.date() == self.election_start.date():
dakiny_logger.debug("Election start.")
await self.election_start_action() await self.election_start_action()
elif today.date() == self.voting_start.date(): elif today.date() == self.voting_start.date():
dakiny_logger.debug("Voting start.")
await self.voting_start_action() await self.voting_start_action()
elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date(): elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date():
dakiny_logger.debug("Election reminder.")
await self.election_reminder() await self.election_reminder()
elif today.date() == self.voting_end.date(): elif today.date() == self.voting_end.date():
dakiny_logger.debug("Voting end.")
await self.voting_end_action() await self.voting_end_action()
elif today.date() == self.highcouncil_election_start.date(): elif today.date() == self.highcouncil_election_start.date():
dakiny_logger.debug("Highcouncil election start.")
await self.highcouncil_election_start_action() await self.highcouncil_election_start_action()
elif today.date() == self.term_start.date(): elif today.date() == self.term_start.date():
dakiny_logger.debug("Term start")
await self.term_start_action() await self.term_start_action()
else:
dakiny_logger.warning("Check date didn't do anything. This should never happen.")
async def election_start_action(self) -> None: async def election_start_action(self) -> None:
"""Performs required actions for when the election cycle starts.""" """Performs required actions for when the election cycle starts."""
@@ -1,11 +1,7 @@
"""Contains various views to be used in the high council nomination process.""" """Contains various views to be used in the high council nomination process."""
import nextcord import nextcord
from nextcord.interactions import Interaction from dakiny_bot.dakiny_settings import settings
from sqlalchemy import select
from sqlalchemy.orm import Session
from dakiny_bot.DakinySettings import settings
from dakiny_bot.database.sql_data_classes import CouncilMembers, engine from dakiny_bot.database.sql_data_classes import CouncilMembers, engine
from dakiny_bot.utils import ( from dakiny_bot.utils import (
NOMINEE_SPLITTER, NOMINEE_SPLITTER,
@@ -13,6 +9,9 @@ from dakiny_bot.utils import (
decorate_dm_nomination, decorate_dm_nomination,
get_election_name, get_election_name,
) )
from nextcord.interactions import Interaction
from sqlalchemy import select
from sqlalchemy.orm import Session
class NominateDMModal(nextcord.ui.Modal): class NominateDMModal(nextcord.ui.Modal):
+3 -4
View File
@@ -1,14 +1,13 @@
"""Contains various views to be used in the nomination process.""" """Contains various views to be used in the nomination process."""
import nextcord import nextcord
from dakiny_bot.dakiny_settings import settings
from dakiny_bot.database.sql_data_classes import CouncilMembers, engine
from dakiny_bot.utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name
from nextcord.interactions import Interaction from nextcord.interactions import Interaction
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from dakiny_bot.DakinySettings import settings
from dakiny_bot.database.sql_data_classes import CouncilMembers, engine
from dakiny_bot.utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name
class NominateModal(nextcord.ui.Modal): class NominateModal(nextcord.ui.Modal):
""" """
@@ -1,12 +1,7 @@
"""Contains various views to be used while voting for high council. Controls the logic when performing the vote.""" """Contains various views to be used while voting for high council. Controls the logic when performing the vote."""
import nextcord import nextcord
from nextcord import SelectOption from dakiny_bot.dakiny_settings import settings
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from dakiny_bot.DakinySettings import settings
from dakiny_bot.database.sql_data_classes import ( from dakiny_bot.database.sql_data_classes import (
CouncilMembers, CouncilMembers,
ElectionInformation, ElectionInformation,
@@ -14,6 +9,10 @@ from dakiny_bot.database.sql_data_classes import (
engine, engine,
) )
from dakiny_bot.utils import VoteType, build_highcouncil_votes_str, get_election_name from dakiny_bot.utils import VoteType, build_highcouncil_votes_str, get_election_name
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
class VoteSelect(nextcord.ui.StringSelect): class VoteSelect(nextcord.ui.StringSelect):
+5 -6
View File
@@ -1,12 +1,7 @@
"""Contains various views to be used while voting. Controls the logic when performing the vote.""" """Contains various views to be used while voting. Controls the logic when performing the vote."""
import nextcord import nextcord
from nextcord import SelectOption from dakiny_bot.dakiny_settings import settings
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from dakiny_bot.DakinySettings import settings
from dakiny_bot.database.sql_data_classes import ( from dakiny_bot.database.sql_data_classes import (
CouncilMembers, CouncilMembers,
ElectionInformation, ElectionInformation,
@@ -14,6 +9,10 @@ from dakiny_bot.database.sql_data_classes import (
engine, engine,
) )
from dakiny_bot.utils import VoteType, build_votes_str, get_election_name from dakiny_bot.utils import VoteType, build_votes_str, get_election_name
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
class VoteSelect(nextcord.ui.StringSelect): class VoteSelect(nextcord.ui.StringSelect):
+2 -9
View File
@@ -6,12 +6,10 @@ from typing import TYPE_CHECKING
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from git import Repo from git import Repo
from git.exc import GitCommandError
from nextcord.ext import commands from nextcord.ext import commands
from dakiny_bot.classes.gitea_webhook import GiteaWebhook from dakiny_bot.classes.gitea_webhook import GiteaWebhook
from dakiny_bot.DakinySettings import settings from dakiny_bot.dakiny_settings import settings
from dakiny_bot.logger import dakiny_logger
from dakiny_bot.utils import REPO_DIR, edit_clan_council_info, update_clan_messages from dakiny_bot.utils import REPO_DIR, edit_clan_council_info, update_clan_messages
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -57,12 +55,7 @@ class ClanMessages(commands.Cog, name="Clan Messages"):
template_updates = {file.split("/")[1] for file in modified_files if "message_templates" in file} template_updates = {file.split("/")[1] for file in modified_files if "message_templates" in file}
channel_updates = {file.split("/")[1] for file in modified_files if file.split("/")[1] not in template_updates} channel_updates = {file.split("/")[1] for file in modified_files if file.split("/")[1] not in template_updates}
try: self.origin.pull()
self.origin.pull()
except GitCommandError:
# I think the git connection timed out. Get a new one.
dakiny_logger.info("Refreshing git connection.")
self.repo_setup()
guild: nextcord.Guild = self.bot.get_guild(settings.params.guild_id) guild: nextcord.Guild = self.bot.get_guild(settings.params.guild_id)
await update_clan_messages(guild, channel_updates) await update_clan_messages(guild, channel_updates)
+1 -1
View File
@@ -4,7 +4,7 @@ import nextcord
from discord import TextInputStyle from discord import TextInputStyle
from nextcord.ext import commands from nextcord.ext import commands
from dakiny_bot.DakinySettings import settings from dakiny_bot.dakiny_settings import settings
from dakiny_bot.utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, record_points from dakiny_bot.utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, record_points
+1 -1
View File
@@ -4,7 +4,7 @@ import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from dakiny_bot.DakinySettings import settings from dakiny_bot.dakiny_settings import settings
from dakiny_bot.database.sql_data_classes import Presences, engine from dakiny_bot.database.sql_data_classes import Presences, engine
+1 -1
View File
@@ -3,7 +3,7 @@
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from dakiny_bot.DakinySettings import settings from dakiny_bot.dakiny_settings import settings
from dakiny_bot.utils import GUILDS from dakiny_bot.utils import GUILDS
+6 -4
View File
@@ -4,8 +4,7 @@ import nextcord
from nextcord.ext import application_checks, commands from nextcord.ext import application_checks, commands
from dakiny_bot.classes.election import Election from dakiny_bot.classes.election import Election
from dakiny_bot.cogs.loops import daily_loop from dakiny_bot.dakiny_settings import settings
from dakiny_bot.DakinySettings import settings
from dakiny_bot.utils import GUILDS, get_active_election_cycle, get_quarter_name from dakiny_bot.utils import GUILDS, get_active_election_cycle, get_quarter_name
ME = 206602713511886849 ME = 206602713511886849
@@ -120,8 +119,11 @@ class TestCog(commands.Cog, name="Test commands."):
Args: Args:
interaction (nextcord.Interaction): Interaction with the message. interaction (nextcord.Interaction): Interaction with the message.
""" """
dl = daily_loop.DailyLoop(self.bot) cycle = Election(
await dl.daily_loop() get_active_election_cycle(),
self.bot.get_guild(settings.params.guild_id),
)
await cycle.check_date()
await interaction.send("Test command sent.", ephemeral=True) await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
+1 -8
View File
@@ -11,9 +11,8 @@ from sqlalchemy.orm import Session
from dakiny_bot.app import birthday_list from dakiny_bot.app import birthday_list
from dakiny_bot.classes.election import Election from dakiny_bot.classes.election import Election
from dakiny_bot.DakinySettings import settings from dakiny_bot.dakiny_settings import settings
from dakiny_bot.database.sql_data_classes import UserBirthday, engine from dakiny_bot.database.sql_data_classes import UserBirthday, engine
from dakiny_bot.logger import dakiny_logger
from dakiny_bot.utils import get_active_election_cycle from dakiny_bot.utils import get_active_election_cycle
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -44,10 +43,8 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
an active election, runs the check_date() function for the election to perform actions. Finally checks if the an active election, runs the check_date() function for the election to perform actions. Finally checks if the
election is over to delete the current election object. election is over to delete the current election object.
""" """
dakiny_logger.debug("Daily loop. Current election: %s.", self.current_election)
if self.current_election is None: if self.current_election is None:
self.current_election = self.get_election() self.current_election = self.get_election()
dakiny_logger.debug("Current election: %s.", self.current_election)
if self.current_election is not None: if self.current_election is not None:
await self.current_election.check_date() await self.current_election.check_date()
@@ -81,17 +78,13 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
Election: Current election cycle, None if there is no election happening. Election: Current election cycle, None if there is no election happening.
""" """
cycle = get_active_election_cycle() cycle = get_active_election_cycle()
dakiny_logger.debug("Current cycle: %s.", cycle)
# prevent exception from using cycle as an Int in an array # prevent exception from using cycle as an Int in an array
if cycle is None: if cycle is None:
dakiny_logger.info("Cycle was None. Returning without an election.")
return None return None
election = Election(cycle, self.bot.get_guild(settings.params.guild_id)) election = Election(cycle, self.bot.get_guild(settings.params.guild_id))
dakiny_logger.debug("Election active: %s", election.active)
return election if election.active else None return election if election.active else None
def update_birthdays(self) -> None: def update_birthdays(self) -> None:
+2 -11
View File
@@ -4,7 +4,6 @@ import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from dakiny_bot.app import birthday_list from dakiny_bot.app import birthday_list
from dakiny_bot.DakinySettings import settings
class SubmissionListener(commands.Cog, name="Message Listener"): class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -17,15 +16,7 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
async def on_message(self, message: nextcord.Message) -> None: async def on_message(self, message: nextcord.Message) -> None:
"""Listener for message creation. Adds score and attachments to submission.""" """Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot. # don't want to process any messages from bot.
if message.author.id == self.bot.user.id: # pyright: ignore[reportOptionalMemberAccess] if message.author.id == self.bot.user.id:
return
# Ban all members who message in this channel. Everyone in guild is a member, so shouldn't miss a ban
# due to message.author not being a nextcord.Member object.
if message.channel.id == settings.channel_ids.ban and isinstance(message.author, nextcord.Member):
await message.author.ban(
delete_message_seconds=30 * 60, reason="Probable spam. The ban channel was messaged."
)
return return
await birthday_action(message) await birthday_action(message)
@@ -40,7 +31,7 @@ def setup(bot: commands.Bot) -> None:
async def birthday_action(message: nextcord.Message) -> None: async def birthday_action(message: nextcord.Message) -> None:
""" """
Checks a message's author and mentions to see if they are in the birthday list. Checks a message's auther and mentions to see if they are in the birthday list.
Args: Args:
message (nextcord.Message): The message to check for a birthday person. message (nextcord.Message): The message to check for a birthday person.
@@ -17,7 +17,6 @@ class ChannelIds:
event_post: int event_post: int
general: int general: int
voting: int voting: int
ban: int
@dataclass @dataclass
@@ -84,4 +83,4 @@ class DakinySettings(YAMLWizard):
self.to_yaml_file(CONFIG_LOCATION) self.to_yaml_file(CONFIG_LOCATION)
settings = DakinySettings.from_yaml_file(CONFIG_LOCATION) settings: DakinySettings | None = None
+51
View File
@@ -0,0 +1,51 @@
"""Broken out util functions to avoid importing sql during testing."""
import calendar
from datetime import UTC, datetime
from dakiny_bot.dakiny_settings import settings
def get_datetime(month_day: str) -> datetime:
"""
Takes in a string month and day and returns a datetime object of that Month/Day this year.
Args:
month_day (str): Month/Day in that format.
Returns:
datetime: Datetime representation of Month/Day.
"""
today = datetime.now(tz=UTC).date()
year = today.year
month = int(month_day.split("/")[0])
day = int(month_day.split("/")[1])
# ensure that year rolls over if current month is December and the next month is less than 12.
if today.month == calendar.DECEMBER and month < calendar.OCTOBER:
year += 1
if today.month == calendar.JANUARY and month > calendar.MARCH:
year -= 1
return datetime(year, month, day, tzinfo=UTC)
def get_quarter_cycle() -> int | None:
"""
Returns the index for the current cycle/quarter. Defined as between term start and end.
Can return None if entire year isn't covered by cycles.
Returns:
int: cycle index for current cycle.
"""
today = datetime.now(tz=UTC)
for cycle, election in enumerate(settings.election_cycles):
if (
today.date() >= get_datetime(election.term_start).date()
and today.date() <= get_datetime(election.term_end).date()
):
return cycle
return None
-13
View File
@@ -1,13 +0,0 @@
"""Creates logger for Dakiny bot."""
import logging
import os
import sys
dakiny_logger = logging.getLogger("default")
dakiny_logger.setLevel(logging.DEBUG if os.getenv("DEBUG") == "true" else logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(
logging.Formatter("%(asctime)s:%(levelname)s [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
)
dakiny_logger.addHandler(handler)
+1
View File
@@ -0,0 +1 @@
"""Empty init for testing util functions."""
+1
View File
@@ -0,0 +1 @@
"""Empty init for testing util functions."""
@@ -0,0 +1,41 @@
"""Tests for get_datetime function."""
import datetime
from freezegun import freeze_time
from dakiny_bot.election_utils import get_datetime
@freeze_time("2025-6-16")
def test_regular_get_datetime() -> None:
"""
Test a regular get_datetime.
Expect the returned datetime to be the frozen date.
Have two different days to ensure that the date is being returned correctly.
"""
date_time = get_datetime("6/15")
assert date_time == datetime.datetime(2025, 6, 15, tzinfo=datetime.UTC)
@freeze_time("2025-1-16")
def test_jan_get_datetime() -> None:
"""
Test previous year get_datetime.
Frozen day is in January 2025, datetime string is in December. Expect datetime to be from 2024.
"""
date_time = get_datetime("12/15")
assert date_time == datetime.datetime(2024, 12, 15, tzinfo=datetime.UTC)
@freeze_time("2025-12-16")
def test_dec_get_datetime() -> None:
"""
Test next year get_datetime.
Frozen day is in December 2025, datetime string is in January. Expect datetime to be from 2026.
"""
date_time = get_datetime("1/15")
assert date_time == datetime.datetime(2026, 1, 15, tzinfo=datetime.UTC)
@@ -0,0 +1,31 @@
"""Test functions for get_quarter_cycle."""
from unittest.mock import mock_open, patch
from freezegun import freeze_time
from dakiny_bot.dakiny_settings import ChannelIds, DakinySettings, Election, Params, PointSettings, RoleIds
from dakiny_bot.election_utils import get_quarter_cycle
# I only care about term start and term end for this test
q1 = Election(True, "", "", "", "", "q1", "4/30", "2/1", "", "") # noqa: FBT003
q2 = Election(True, "", "", "", "", "q2", "7/31", "5/1", "", "") # noqa: FBT003
q3 = Election(True, "", "", "", "", "q3", "10/31", "8/1", "", "") # noqa: FBT003
q4 = Election(True, "", "", "", "", "q4", "1/31", "11/1", "", "") # noqa: FBT003
# Dummy all the values I don't care about.
settings = DakinySettings(
ChannelIds(1, 1, 1, 1, 1, 1),
[q1, q2, q3, q4],
Params("", "", 1),
PointSettings(1, 1, 1, 1, 1, True, True), # noqa: FBT003
RoleIds(1, 1, 1, 1, 1),
)
@patch("election_utils.get_quarter_cycle.settings", settings)
@patch("builtins.open", new_callable=mock_open)
class TestGetQuarterCycle:
@freeze_time("2025-2-25")
def test_regular_q1(self) -> None:
assert get_quarter_cycle() == 0
+11 -58
View File
@@ -1,6 +1,5 @@
"""File contains util functions.""" """File contains util functions."""
import calendar
import os import os
from collections.abc import Sequence from collections.abc import Sequence
from datetime import UTC, datetime from datetime import UTC, datetime
@@ -20,9 +19,9 @@ from dakiny_bot.database.sql_data_classes import (
Votes, Votes,
engine, engine,
) )
from dakiny_bot.logger import dakiny_logger
from .DakinySettings import settings from .dakiny_settings import settings
from .election_utils import get_datetime, get_quarter_cycle
NOMINEE_SPLITTER = "========================================\n\n" NOMINEE_SPLITTER = "========================================\n\n"
REPO_DIR = "./dakiny-messages" REPO_DIR = "./dakiny-messages"
@@ -35,33 +34,6 @@ VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
PointType = StrEnum("PointType", ["CLAN", "EVENT"]) PointType = StrEnum("PointType", ["CLAN", "EVENT"])
def get_datetime(month_day: str) -> datetime:
"""
Takes in a string month and day and returns a datetime object of that Month/Day this year.
Args:
month_day (str): Month/Day in that format.
Returns:
datetime: Datetime representation of Month/Day.
"""
today = datetime.now(tz=UTC).date()
year = today.year
month = int(month_day.split("/")[0])
day = int(month_day.split("/")[1])
# ensure that year rolls over if current month is December and the next month is less than 12.
if today.month == calendar.DECEMBER and month < calendar.OCTOBER:
year += 1
if today.month == calendar.JANUARY and month > calendar.MARCH:
year -= 1
try:
return datetime(year, month, day, tzinfo=UTC)
except ValueError:
dakiny_logger.warning("Invalid month day: %s.", month_day)
def get_election_name() -> str: def get_election_name() -> str:
""" """
Builds the unique name of the current election cycle. Builds the unique name of the current election cycle.
@@ -69,7 +41,10 @@ def get_election_name() -> str:
Returns: Returns:
str: the name of the election cycle str: the name of the election cycle
""" """
cycle = get_active_election_cycle() if cycle := get_active_election_cycle() is None:
# undefined behavior
print("Did not find an active election cycle. Undefined behavior with return string.") # noqa: T201
return "No-active-election-cycle"
current_cycle = settings.election_cycles[cycle] current_cycle = settings.election_cycles[cycle]
return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}" return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
@@ -81,13 +56,16 @@ def get_quarter_name() -> str:
Returns: Returns:
str: the name of the cycle. str: the name of the cycle.
""" """
cycle = get_quarter_cycle() if cycle := get_quarter_cycle() is None:
# undefined behavior
print("Did not find a quarter cycle. Undefined behavior with return string.") # noqa: T201
return "no-quarter-cycle"
current_cycle = settings.election_cycles[cycle] current_cycle = settings.election_cycles[cycle]
return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}" return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
def get_active_election_cycle() -> int: def get_active_election_cycle() -> int | None:
""" """
Returns index for active election. An election is considered active between the election start and the term start. Returns index for active election. An election is considered active between the election start and the term start.
@@ -95,43 +73,18 @@ def get_active_election_cycle() -> int:
int: cycle index for active election. None for no active cycle int: cycle index for active election. None for no active cycle
""" """
today = datetime.now(tz=UTC) today = datetime.now(tz=UTC)
dakiny_logger.debug("Today is %s", today)
for cycle, election in enumerate(settings.election_cycles): for cycle, election in enumerate(settings.election_cycles):
if ( if (
today.date() >= get_datetime(election.election_start).date() today.date() >= get_datetime(election.election_start).date()
and today.date() <= get_datetime(election.term_start).date() and today.date() <= get_datetime(election.term_start).date()
): ):
dakiny_logger.debug("Cycle is %d", cycle)
return cycle return cycle
# It isn't a current election, get the quarter # It isn't a current election, get the quarter
return get_quarter_cycle() return get_quarter_cycle()
def get_quarter_cycle() -> int:
"""
Returns the index for the current cycle/quarter. Defined as between term start and end.
Can return None if entire year isn't covered by cycles.
Returns:
int: cycle index for current cycle.
"""
today = datetime.now(tz=UTC)
dakiny_logger.debug("Today is %s", today)
for cycle, election in enumerate(settings.election_cycles):
if (
today.date() >= get_datetime(election.term_start).date()
and today.date() <= get_datetime(election.term_end).date()
):
dakiny_logger.debug("Cycle is %d", cycle)
return cycle
dakiny_logger.debug("Quarter cycle is None.")
return None
def build_votes_str(guild: nextcord.Guild) -> str: def build_votes_str(guild: nextcord.Guild) -> str:
""" """
Creates a string that contains all nominees and their votes to be sent as a discord message. Creates a string that contains all nominees and their votes to be sent as a discord message.
+12 -5
View File
@@ -1,18 +1,17 @@
[project] [project]
authors = [{ name = "imcompiling", email = "[email protected]" }] authors = [{ name = "imcompiling", email = "[email protected]" }]
requires-python = "<4.0,>=3.13" requires-python = "<4.0,>=3.12"
dependencies = [ dependencies = [
"gitpython<4.0.0,>=3.1.43", "gitpython<4.0.0,>=3.1.43",
"fastapi<1.0.0,>=0.111.0", "fastapi<1.0.0,>=0.111.0",
"nextcord<3.0.0,>=2.6.0", "nextcord<3.0.0,>=2.6.0",
"pandas<3.1.0,>=3.0.6", "pandas<3.0.0,>=2.2.2",
"pytz<2025.3,>=2025.2", "pytz<2025.3,>=2025.2",
"sqlalchemy<3.0.0,>=2.0.30", "sqlalchemy<3.0.0,>=2.0.30",
"aiofiles<24.0.0,>=23.2.1", "aiofiles<24.0.0,>=23.2.1",
"setuptools<80.10.3,>=80.10.2", "setuptools<71.0.0,>=70.0.0",
"dataclass-wizard[yaml]>=0.22.3,<1.0.0", "dataclass-wizard[yaml]>=0.22.3,<1.0.0",
"uvicorn>=0.34.0", "uvicorn>=0.34.0",
"audioop-lts>=0.2.1",
] ]
name = "dakiny-bot" name = "dakiny-bot"
version = "0.1.0" version = "0.1.0"
@@ -20,7 +19,12 @@ description = "Dakiny's Discord Bot."
readme = "README.md" readme = "README.md"
[dependency-groups] [dependency-groups]
dev = ["ruff<1.0.0,>=0.6.9"] dev = [
"freezegun>=1.5.1",
"pytest>=8.3.5",
"pytest-cov>=6.1.1",
"ruff<1.0.0,>=0.6.9",
]
[tool.ruff] [tool.ruff]
# Exclude a variety of commonly ignored directories. # Exclude a variety of commonly ignored directories.
@@ -127,3 +131,6 @@ docstring-code-format = true
# This only has an effect when the `docstring-code-format` setting is # This only has an effect when the `docstring-code-format` setting is
# enabled. # enabled.
docstring-code-line-length = "dynamic" docstring-code-line-length = "dynamic"
[tool.ruff.lint.per-file-ignores]
"**/{tests}/*" = ["ANN001", "ANN201", "S101", "PLR2004"]
+3 -12
View File
@@ -1,11 +1,8 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"lockFileMaintenance": { "lockFileMaintenance": {
"automerge": true "automerge": true,
}, "enabled": true
"major": {
"automerge": false,
"automergeType": "pr"
}, },
"minor": { "minor": {
"automerge": true, "automerge": true,
@@ -19,11 +16,5 @@
"automerge": true, "automerge": true,
"automergeType": "branch" "automergeType": "branch"
}, },
"ignoreTests": true, "ignoreTests": true
"dockerfile": {
"minor": {
"automerge": false,
"automergeType": "pr"
}
}
} }
Generated
+397 -383
View File
File diff suppressed because it is too large Load Diff