From 4bd937a5707e7703ee63e6804dc92464da63e41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Sun, 26 Jan 2025 04:34:23 +0100 Subject: [PATCH] Add Loguru for enhanced logging and update logging statements for clarity --- .vscode/settings.json | 1 + discord_reminder_bot/main.py | 119 ++++++++++++++++----------------- discord_reminder_bot/misc.py | 8 +-- discord_reminder_bot/parser.py | 10 +-- discord_reminder_bot/ui.py | 75 ++++++++++----------- pyproject.toml | 8 +++ 6 files changed, 113 insertions(+), 108 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 58bda0c..6690fa8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,7 @@ "jobstore", "jobstores", "levelname", + "loguru", "Lovinator", "pycodestyle", "pydocstyle", diff --git a/discord_reminder_bot/main.py b/discord_reminder_bot/main.py index 4059ba4..c46fdb2 100644 --- a/discord_reminder_bot/main.py +++ b/discord_reminder_bot/main.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import datetime import json -import logging import tempfile from pathlib import Path from typing import TYPE_CHECKING, Any @@ -13,6 +12,7 @@ import sentry_sdk from apscheduler.job import Job from discord.abc import PrivateChannel from discord_webhook import DiscordWebhook +from loguru import logger from discord_reminder_bot.misc import calc_time, calculate from discord_reminder_bot.parser import parse_time @@ -28,17 +28,13 @@ if TYPE_CHECKING: from discord_reminder_bot import settings -logger: logging.Logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) -logging.getLogger("discord.client").setLevel(logging.INFO) sentry_sdk.init( dsn="https://c4c61a52838be9b5042144420fba5aaa@o4505228040339456.ingest.us.sentry.io/4508707268984832", traces_sample_rate=1.0, + send_default_pii=True, ) -GUILD_ID = discord.Object(id=341001473661992962) - scheduler: settings.AsyncIOScheduler = get_scheduler() msg_to_cleanup: list[discord.InteractionMessage] = [] @@ -58,7 +54,7 @@ class RemindBotClient(discord.Client): async def on_error(self, event_method: str, *args: list[Any], **kwargs: dict[str, Any]) -> None: """Log errors that occur in the bot.""" # Log the error - logger.exception("An error occurred in %s", event_method) + logger.exception(f"An error occurred in {event_method} with args: {args} and kwargs: {kwargs}") # Add context to Sentry with sentry_sdk.push_scope() as scope: @@ -100,26 +96,26 @@ class RemindBotClient(discord.Client): async def on_ready(self) -> None: """Log when the bot is ready.""" - logger.info("Logged in as %s (%s)", self.user, self.user.id if self.user else "N/A ID") + logger.info(f"Logged in as {self.user} ({self.user.id if self.user else 'Unknown'})") async def close(self) -> None: """Close the bot and cleanup views.""" logger.info("Closing bot and cleaning up views.") for msg in msg_to_cleanup: - logger.debug("Removing view: %s", msg.id) + logger.debug(f"Removing view: {msg.id}") try: # If the message is "/remind list timed out.", skip it - if "/remind list timed out." in msg.content: - logger.debug("Message %s is a timeout message. Skipping.", msg.id) + if "`/remind list` timed out." in msg.content: + logger.debug(f"Message {msg.id} is a timeout message. Skipping.") continue await msg.delete() except discord.HTTPException as e: if e.status != 401: # Skip if the webhook token is invalid - logger.error("Failed to remove view: %s", e) # noqa: TRY400 + logger.error(f"Failed to remove view: {msg.id} - {e.text} - {e.status} - {e.code}") except asyncio.exceptions.CancelledError: - logger.error("Failed to remove view: Task was cancelled.") # noqa: TRY400 + logger.error("Failed to remove view: Task was cancelled.") return await super().close() @@ -138,13 +134,12 @@ class RemindBotClient(discord.Client): time: str = "Paused" if hasattr(job, "next_run_time") and job.next_run_time and isinstance(job.next_run_time, datetime.datetime): time = job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") + logger.info(rf"\t{job.id}: {job.name} - {time} - {msg}") - logger.info("\t%s: %s (%s)", msg[:50] or "No message", time, job.id) - except Exception: + except (AttributeError, LookupError): logger.exception("Failed to loop through jobs") - self.tree.copy_global_to(guild=GUILD_ID) - await self.tree.sync(guild=GUILD_ID) + await self.tree.sync() class RemindGroup(discord.app_commands.Group): @@ -178,8 +173,8 @@ class RemindGroup(discord.app_commands.Group): # TODO(TheLovinator): Check if we have access to the channel and user # noqa: TD003 await interaction.response.defer() - logger.info("New reminder from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel) - logger.info("Arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None}) + logger.info(f"New reminder from {interaction.user} ({interaction.user.id}) in {interaction.channel}") + logger.info(f"Arguments: {locals()}") # Check if we have access to the specified channel or the current channel target_channel: InteractionChannel | None = channel or interaction.channel @@ -218,7 +213,7 @@ class RemindGroup(discord.app_commands.Group): "message": message, }, ) - logger.info("User reminder job created: %s for %s at %s", user_reminder, user.id, time) + logger.info(f"User reminder job created: {user_reminder} for {user.id} at {parsed_time}") dm_message = f" and a DM to {user.display_name}" if not dm_and_current_channel: @@ -239,7 +234,7 @@ class RemindGroup(discord.app_commands.Group): "author_id": interaction.user.id, }, ) - logger.info("Channel reminder job created: %s for %s at %s", channel_job, channel_id, time) + logger.info(f"Channel reminder job created: {channel_job} for {channel_id}") msg: str = ( f"Hello {interaction.user.display_name},\n" @@ -251,7 +246,7 @@ class RemindGroup(discord.app_commands.Group): # /remind event @discord.app_commands.command(name="event", description="Add a new Discord event.") - async def add_event( # noqa: PLR0913, PLR0917, PLR6301 + async def add_event( # noqa: C901, PLR0913, PLR0917, PLR6301 self, interaction: discord.Interaction, message: str, @@ -272,8 +267,8 @@ class RemindGroup(discord.app_commands.Group): """ await interaction.response.defer() - logger.info("New event from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel) - logger.info("Arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None}) + logger.info(f"New event from {interaction.user} ({interaction.user.id}) in {interaction.channel}") + logger.info(f"Arguments: {locals()}") guild: discord.Guild | None = interaction.guild if not guild: @@ -296,15 +291,19 @@ class RemindGroup(discord.app_commands.Group): reason_msg: str = f"Event created by {interaction.user} ({interaction.user.id})." - event: discord.ScheduledEvent = await guild.create_scheduled_event( - name=message, - start_time=event_start_time, - entity_type=discord.EntityType.external, - privacy_level=discord.PrivacyLevel.guild_only, - end_time=event_end_time, - reason=reason or reason_msg, - location=location, - ) + try: + event: discord.ScheduledEvent = await guild.create_scheduled_event( + name=message, + start_time=event_start_time, + entity_type=discord.EntityType.external, + privacy_level=discord.PrivacyLevel.guild_only, + end_time=event_end_time, + reason=reason or reason_msg, + location=location, + ) + except discord.Forbidden as e: + await interaction.followup.send(content=f"I don't have permission to create events in this guild: {e}", ephemeral=True) + return if start_immediately: await event.start() @@ -354,12 +353,12 @@ class RemindGroup(discord.app_commands.Group): for job in jobs: # If the job has guild_id and it's not the current guild, skip it if job.kwargs.get("guild_id") and job.kwargs.get("guild_id") != guild.id: - logger.debug("Skipping job: %s because it's not in the current guild.", job.id) + logger.debug(f"Skipping job: {job.id} because it's not in the current guild.") continue # If the job has channel_id and it's not in the current guild, skip it if job.kwargs.get("channel_id") and job.kwargs.get("channel_id") not in list_of_channels_in_current_guild: - logger.debug("Skipping job: %s because it's not in the current guild's channels.", job.id) + logger.debug(f"Skipping job: {job.id} because it's not in the current guild.") continue jobs_in_guild.append(job) @@ -421,15 +420,15 @@ class RemindGroup(discord.app_commands.Group): try: await interaction.response.defer() except discord.HTTPException as e: - logger.exception("Failed to defer interaction: text=%s, status=%s, code=%s", e.text, e.status, e.code) + logger.exception(f"Failed to defer interaction: {e.text=}, {e.status=}, {e.code=}") return except discord.InteractionResponded as e: - logger.exception("Interaction already responded to - interaction: %s", e.interaction) + logger.exception(f"Interaction already responded to - interaction: {interaction}, {e}") return # Log kwargs - logger.info("New cron job from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel) - logger.info("Cron job arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None}) + logger.info(f"New cron job from {interaction.user} ({interaction.user.id}) in {interaction.channel}") + logger.info(f"Cron job arguments: {locals()}") # Get the channel ID channel_id: int | None = channel.id if channel else (interaction.channel.id if interaction.channel else None) @@ -548,8 +547,8 @@ class RemindGroup(discord.app_commands.Group): """ # noqa: E501 await interaction.response.defer() - logger.info("New interval job from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel) - logger.info("Arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None}) + logger.info(f"New interval job from {interaction.user} ({interaction.user.id}) in {interaction.channel}") + logger.info(f"Arguments: {locals()}") # Only allow intervals of 30 seconds or more so we don't spam the channel if weeks == days == hours == minutes == 0 and seconds < 30: @@ -673,18 +672,18 @@ class RemindGroup(discord.app_commands.Group): # Can't be 0 because that's the default value for jobs without a guild guild_id: int = interaction.guild.id if interaction.guild else -1 channels_in_this_guild: list[int] = [c.id for c in interaction.guild.channels] if interaction.guild else [] - logger.debug("Guild ID: %s, Channels in this guild: %s", guild_id, channels_in_this_guild) + logger.debug(f"Guild ID: {guild_id}") for job in jobs_data.get("jobs", []): # Check if the job is in the current guild job_guild_id = job.get("kwargs", {}).get("guild_id", 0) if job_guild_id and job_guild_id != guild_id: - logger.debug("Removing job: %s because it's not in the current guild. %s vs %s", job.get("id"), job_guild_id, guild_id) + logger.debug(f"Removing job: {job.get('id')} because it's not in the current guild.") jobs_data["jobs"].remove(job) # Check if the channel is in the current guild if job.get("kwargs", {}).get("channel_id") not in channels_in_this_guild: - logger.debug("Removing job: %s because it's not in the current guild's channels.", job.get("id")) + logger.debug(f"Removing job: {job.get('id')} because it's not in the current guild.") jobs_data["jobs"].remove(job) msg: str = "All reminders in this server have been backed up." if not all_servers else "All reminders have been backed up." @@ -712,7 +711,7 @@ class RemindGroup(discord.app_commands.Group): """ await interaction.response.defer() - logger.info("Restoring reminders from file for %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel) + logger.info(f"Restoring reminders from file for {interaction.user} ({interaction.user.id}) in {interaction.channel}") # Get the old jobs old_jobs: list[Job] = scheduler.get_jobs() @@ -755,7 +754,7 @@ class RemindGroup(discord.app_commands.Group): # Save the file to a temporary file and import the jobs with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding="utf-8", suffix=".json") as temp_file: - logger.info("Saving attachment to %s", temp_file.name) + logger.info(f"Saving attachment to {temp_file.name}") await attachment.save(Path(temp_file.name)) # Load the jobs data from the file @@ -763,7 +762,7 @@ class RemindGroup(discord.app_commands.Group): jobs_data: dict = json.load(temp_file) logger.info("Importing jobs from file") - logger.debug("Jobs data: %s", jobs_data) + logger.debug(f"Jobs data: {jobs_data}") with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding="utf-8", suffix=".json") as temp_import_file: # We can't import jobs with the same ID so remove them from the JSON @@ -771,10 +770,10 @@ class RemindGroup(discord.app_commands.Group): jobs_already_exist = [job.get("id") for job in jobs_data.get("jobs", []) if scheduler.get_job(job.get("id"))] jobs_data["jobs"] = jobs for job_id in jobs_already_exist: - logger.debug("Removed job: %s because it already exists.", job_id) + logger.debug(f"Removed job: {job_id} because it already exists.") - logger.debug("Jobs data after removing existing jobs: %s", jobs_data) - logger.info("Jobs already exist: %s", jobs_already_exist) + logger.debug(f"Jobs data after removing existing jobs: {jobs_data}") + logger.info(f"Jobs already exist: {jobs_already_exist}") # Write the new data to a temporary file json.dump(jobs_data, temp_import_file) @@ -823,7 +822,7 @@ def send_webhook(url: str = "", message: str = "") -> None: if not url: url = get_webhook_url() - logger.error("No webhook URL provided. Using the one from settings.") + logger.error(f"No webhook URL provided. Using the one from settings: {url}") webhook: DiscordWebhook = DiscordWebhook( url=url, username="discord-reminder-bot", @@ -851,7 +850,7 @@ async def send_to_discord(channel_id: int, message: str, author_id: int) -> None # Channels we can't send messages to if isinstance(channel, discord.ForumChannel | discord.CategoryChannel | PrivateChannel): - logger.warning("We haven't implemented sending messages to this channel type (%s)", type(channel)) + logger.warning(f"We haven't implemented sending messages to this channel type {type(channel)}") return await channel.send(f"<@{author_id}>\n{message}") @@ -865,17 +864,17 @@ async def send_to_user(user_id: int, guild_id: int, message: str) -> None: guild_id: The guild ID to get the user from. message: The message to send. """ - logger.info("Sending message to user %s in guild %s:\n%s", user_id, guild_id, message) + logger.info(f"Sending message to user {user_id} in guild {guild_id}") try: guild: discord.Guild | None = bot.get_guild(guild_id) if guild is None: guild = await bot.fetch_guild(guild_id) except discord.NotFound: current_guilds: Sequence[discord.Guild] = bot.guilds - logger.exception("Guild not found. Current guilds: %s", current_guilds) + logger.exception(f"Guild not found. Current guilds: {current_guilds}") return except discord.HTTPException: - logger.exception("Failed to fetch guild") + logger.exception(f"Failed to fetch guild {guild_id}") return try: @@ -883,21 +882,21 @@ async def send_to_user(user_id: int, guild_id: int, message: str) -> None: if member is None: member = await guild.fetch_member(user_id) except discord.Forbidden: - logger.exception("We do not have access to the guild. Guild: %s, User: %s", guild_id, user_id) + logger.exception(f"We do not have access to the guild. Guild: {guild_id}, User: {user_id}") return except discord.NotFound: - logger.exception("Member not found. Guild: %s, User: %s", guild_id, user_id) + logger.exception(f"Member not found. Guild: {guild_id}, User: {user_id}") return except discord.HTTPException: - logger.exception("Fetching the member failed. Guild: %s, User: %s", guild_id, user_id) + logger.exception(f"Fetching the member failed. Guild: {guild_id}, User: {user_id}") return try: await member.send(message) except discord.HTTPException: - logger.exception("Failed to send message (%s) to user (%s) in guild (%s)", message, user_id, guild_id) + logger.exception(f"Failed to send message '{message}' to user '{user_id}' in guild '{guild_id}'") if __name__ == "__main__": bot_token: str = get_bot_token() - bot.run(bot_token, root_logger=True) + bot.run(bot_token) diff --git a/discord_reminder_bot/misc.py b/discord_reminder_bot/misc.py index fc53bce..1fa584b 100644 --- a/discord_reminder_bot/misc.py +++ b/discord_reminder_bot/misc.py @@ -1,17 +1,15 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING from apscheduler.triggers.date import DateTrigger +from loguru import logger if TYPE_CHECKING: import datetime from apscheduler.job import Job -logger: logging.Logger = logging.getLogger(__name__) - def calculate(job: Job) -> str | None: """Calculate the time left for a job. @@ -26,8 +24,8 @@ def calculate(job: Job) -> str | None: # Check if the job is paused if trigger_time is None: - logger.error("Couldn't calculate time for job: %s: %s", job.id, job.name) - logger.error("State: %s", job.__getstate__() if hasattr(job, "__getstate__") else "No state") + logger.error(f"Couldn't calculate time for job: {job.id}") + logger.error(f"State: {job.__getstate__() if hasattr(job, '__getstate__') else 'No state'}") return None return f"" diff --git a/discord_reminder_bot/parser.py b/discord_reminder_bot/parser.py index 9850d22..3d23220 100644 --- a/discord_reminder_bot/parser.py +++ b/discord_reminder_bot/parser.py @@ -1,15 +1,13 @@ from __future__ import annotations import datetime -import logging from zoneinfo import ZoneInfo, ZoneInfoNotFoundError import dateparser +from loguru import logger from discord_reminder_bot.settings import get_timezone -logger: logging.Logger = logging.getLogger(__name__) - def parse_time(date_to_parse: str | None, timezone: str | None = None, use_dotenv: bool = True) -> datetime.datetime | None: # noqa: FBT001, FBT002 """Parse a date string into a datetime object. @@ -22,7 +20,7 @@ def parse_time(date_to_parse: str | None, timezone: str | None = None, use_doten Returns: datetime.datetime: The parsed datetime object. """ - logger.info("Parsing date: '%s' with timezone: '%s'", date_to_parse, timezone) + logger.info(f"Parsing date: '{date_to_parse}' with timezone: '{timezone}'") if not date_to_parse: logger.error("No date provided to parse.") @@ -35,7 +33,7 @@ def parse_time(date_to_parse: str | None, timezone: str | None = None, use_doten try: tz = ZoneInfo(timezone) except (ZoneInfoNotFoundError, ModuleNotFoundError): - logger.error("Invalid timezone provided: '%s'. Using default timezone: '%s'", timezone, get_timezone(use_dotenv)) # noqa: TRY400 + logger.error(f"Invalid timezone provided: '{timezone}'. Using {get_timezone(use_dotenv)} instead.") tz = ZoneInfo("UTC") try: @@ -51,4 +49,6 @@ def parse_time(date_to_parse: str | None, timezone: str | None = None, use_doten except (ValueError, TypeError): return None + logger.debug(f"Parsed date: {parsed_date} from '{date_to_parse}'") + return parsed_date diff --git a/discord_reminder_bot/ui.py b/discord_reminder_bot/ui.py index 7b734bd..8faac34 100644 --- a/discord_reminder_bot/ui.py +++ b/discord_reminder_bot/ui.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING import discord @@ -10,6 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger from discord.ui import Button, Select +from loguru import logger from discord_reminder_bot.misc import DateTrigger, calc_time, calculate from discord_reminder_bot.parser import parse_time @@ -21,9 +21,6 @@ if TYPE_CHECKING: from apscheduler.schedulers.asyncio import AsyncIOScheduler -logger: logging.Logger = logging.getLogger(__name__) - - class ModifyJobModal(discord.ui.Modal, title="Modify Job"): """Modal for modifying a job.""" @@ -53,12 +50,12 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"): self.job_name.placeholder = self.job.kwargs.get("message", "No message found") self.job_date.placeholder = self.job.next_run_time.strftime("%Y-%m-%d %H:%M:%S %Z") - logger.info("Job '%s' Modal created", self.job.name) - logger.info("\tCurrent date: '%s'", self.job.next_run_time) - logger.info("\tCurrent message: '%s'", self.job.kwargs.get("message", "N/A")) + logger.info(f"Job '{job_name_label}' modified: Initializing modal") + logger.info(f"\tCurrent date: '{self.job.next_run_time}'") + logger.info(f"\tCurrent message: '{self.job.kwargs.get('message', 'No message found')}") - logger.info("\tName label: '%s'", self.job_name.label) - logger.info("\tDate label: '%s'", self.job_date.label) + logger.info(f"\tName label: '{self.job_name.label}'") + logger.info(f"\tDate label: '{self.job_date.label}'") async def on_submit(self, interaction: discord.Interaction) -> None: """Submit the job modifications. @@ -66,27 +63,29 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"): Args: interaction: The interaction object for the command. """ - logger.info("Job '%s' modified: Submitting changes", self.job.name) + job_msg: str = self.job.kwargs.get("message", "No message found") + logger.info(f"Job '{job_msg}' modified: Submitting changes") new_name: str = self.job_name.value new_date_str: str = self.job_date.value old_date: datetime.datetime = self.job.next_run_time # if both are empty, do nothing if not new_name and not new_date_str: - logger.info("Job '%s' modified: No changes submitted", self.job.name) + logger.info(f"Job '{job_msg}' modified: No changes submitted.") await interaction.response.send_message( - content=f"Job **{self.job.name}** was not modified by {interaction.user.mention}.\nNo changes submitted.", + content=f"Job **{job_msg}**.\nNo changes submitted.", + ephemeral=True, ) return if new_date_str and new_date_str != old_date.strftime("%Y-%m-%d %H:%M:%S %Z"): new_date: datetime.datetime | None = parse_time(new_date_str) if not new_date: - logger.error("Job '%s' modified: Failed to parse date: '%s'", self.job.name, new_date_str) + logger.error(f"Job '{job_msg}' modified: Failed to parse date: '{new_date_str}'") await interaction.response.send_message( content=( - f"Failed modifying job **{self.job.name}**\n" + f"Failed modifying job **{job_msg}**\n" f"Job ID: **{self.job.id}**\n" f"Failed to parse date: **{new_date_str}**\n" f"Defaulting to old date: **{old_date.strftime('%Y-%m-%d %H:%M:%S')}** {calc_time(old_date)}" @@ -94,8 +93,8 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"): ) return - logger.info("Job '%s' modified: New date: '%s'", self.job.name, new_date) - logger.info("Job '%s' modified: Old date: '%s'", self.job.name, old_date) + logger.info(f"Job '{job_msg}' modified: New date: '{new_date}'") + logger.info(f"Job '{job_msg}' modified: Old date: '{old_date}'") self.job.modify(next_run_time=new_date) old_date_str: str = old_date.strftime("%Y-%m-%d %H:%M:%S") @@ -103,16 +102,16 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"): await interaction.response.send_message( content=( - f"Job **{self.job.name}** was modified by {interaction.user.mention}:\n" + f"Job **{job_msg}** was modified by {interaction.user.mention}:\n" f"Job ID: **{self.job.id}**\n" f"Old date: **{old_date_str}** {calculate(self.job)} {calc_time(old_date)}\n" f"New date: **{new_date_str}** {calculate(self.job)} {calc_time(new_date)}" ), ) - if self.job_name.value and self.job.name != new_name: - logger.info("Job '%s' modified: New name: '%s'", self.job.name, new_name) - logger.info("Job '%s' modified: Old name: '%s'", self.job.name, self.job.name) + if self.job_name.value and job_msg != new_name: + logger.info(f"Job '{job_msg}' modified: New name: '{new_name}'") + logger.info(f"Job '{job_msg}' modified: Old name: '{job_msg}'") self.job.modify(name=new_name) await interaction.response.send_message( @@ -187,12 +186,12 @@ class JobSelector(Select): for job in jobs: # If the job has guild_id and it's not the current guild, skip it if job.kwargs.get("guild_id") and job.kwargs.get("guild_id") != guild.id: - logger.debug("Skipping job: %s because it's not in the current guild.", job.id) + logger.debug(f"Skipping job: {job.id} because it's not in the current guild.") continue # If the job has channel_id and it's not in the current guild, skip it if job.kwargs.get("channel_id") and job.kwargs.get("channel_id") not in list_of_channels_in_current_guild: - logger.debug("Skipping job: %s because it's not in the current guild's channels.", job.id) + logger.debug(f"Skipping job: {job.id} because it's not from a channel in the current guild.") continue jobs_in_guild.append(job) @@ -252,14 +251,14 @@ class JobManagementView(discord.ui.View): self.add_item(JobSelector(scheduler, self.guild)) self.update_buttons() - logger.debug("JobManagementView created for job: %s", job.id) + logger.debug(f"JobManagementView created for job: {self.job.id}") async def on_timeout(self) -> None: """Handle the view timeout.""" if self.message: await self.message.edit(content="`/remind list` timed out.", embed=None, view=None) else: - logger.debug("No message to edit for job: %s", self.job.id) + logger.debug(f"No message to edit for job: {self.job.id}") self.stop() async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: @@ -312,9 +311,9 @@ class JobManagementView(discord.ui.View): """ job_kwargs: dict = self.job.kwargs or {} - logger.info("Deleting job: %s because %s clicked the button.", self.job.id, interaction.user.name) + logger.info(f"Deleting job: {self.job.id}. Clicked by {interaction.user.name}") if hasattr(self.job, "__getstate__"): - logger.debug("State: %s", self.job.__getstate__() if hasattr(self.job, "__getstate__") else "No state") + logger.debug(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}") job_msg: str | int = job_kwargs.get("message", "No message found") msg: str = f"**Job '{job_msg}' has been deleted.**\n" @@ -346,7 +345,7 @@ class JobManagementView(discord.ui.View): if job_kwargs.get("guild_id"): msg += f"**Guild**: {job_kwargs.get('guild_id')}\n" - logger.debug("Deletion message: %s", msg) + logger.debug(f"Deletion message: {msg}") self.job.remove() await interaction.response.send_message(msg) @@ -360,9 +359,9 @@ class JobManagementView(discord.ui.View): interaction: The interaction object for the command. button: The button that was clicked. """ - logger.info("Modifying job: %s. Clicked by %s", self.job.id, interaction.user.name) + logger.info(f"Modifying job: {self.job.id}. Clicked by {interaction.user.name}") if hasattr(self.job, "__getstate__"): - logger.debug("State: %s", self.job.__getstate__() if hasattr(self.job, "__getstate__") else "No state") + logger.debug(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}") modal = ModifyJobModal(self.job, self.scheduler) await interaction.response.send_modal(modal) @@ -377,17 +376,17 @@ class JobManagementView(discord.ui.View): """ if hasattr(self.job, "next_run_time"): if self.job.next_run_time is None: - logger.info("State: %s", self.job.__getstate__()) + logger.info(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}") self.job.resume() status = "resumed" button.label = "Pause" else: - logger.info("State: %s", self.job.__getstate__()) + logger.info(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}") self.job.pause() status = "paused" button.label = "Resume" else: - logger.error("Got a job without a next_run_time: %s", self.job.id) + logger.error(f"Got a job without a next_run_time: {self.job.id}") status: str = f"What is this? {self.job.__getstate__()}" button.label = "What?" @@ -409,11 +408,11 @@ class JobManagementView(discord.ui.View): def update_buttons(self) -> None: """Update the visibility of buttons based on job status.""" - logger.debug("Updating buttons for job: %s", self.job.id) + logger.debug(f"Updating buttons for job: {self.job.id}") self.pause_button.label = "Resume" if self.job.next_run_time is None else "Pause" - logger.debug("Pause button disabled: %s", self.pause_button.disabled) - logger.debug("Pause button label: %s", self.pause_button.label) + logger.debug(f"Pause button disabled: {self.pause_button.disabled}") + logger.debug(f"Pause button label: {self.pause_button.label}") async def interaction_check(self, interaction: discord.Interaction) -> bool: # noqa: ARG002 """Check the interaction and update buttons before responding. @@ -424,11 +423,11 @@ class JobManagementView(discord.ui.View): Returns: bool: Whether the interaction is valid. """ - logger.info("Interaction check for job: %s", self.job.id) - logger.debug("Timeout was %s before interaction check", self.timeout) + logger.info(f"Interaction check for job: {self.job.id}") + logger.debug(f"Timeout was {self.timeout} before interaction check.") self.timeout = 30 - logger.debug("Checking interaction for job: %s", self.job.id) + logger.debug(f"Checking interaction for job: {self.job.id}") self.update_buttons() return True diff --git a/pyproject.toml b/pyproject.toml index a50b2c2..3212db4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ dependencies = [ # For error tracking "sentry-sdk>=2.20.0,<3.0.0", # https://github.com/getsentry/sentry-python + + # For logging + "loguru>=0.7.3,<1.0.0", # https://github.com/Delgan/loguru ] [dependency-groups] @@ -75,9 +78,14 @@ discord-webhook = {version = ">=1.3.1,<2.0.0"} # For loading environment variables from a .env file python-dotenv = {version = ">=1.0.1,<2.0.0"} +# https://github.com/getsentry/sentry-python # For error tracking sentry-sdk = {version = ">=2.20.0,<3.0.0"} +# https://github.com/Delgan/loguru +# For logging +loguru = {version = ">=0.7.3,<1.0.0"} + [tool.poetry.dev-dependencies] pytest = "*" pre-commit = "*"