Added pydocstyle (D) rules to Ruff and fixed all violations.
CI / Formatting (push) Successful in 12s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 26s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 25s
CI / Type Checking (push) Successful in 26s

This commit is contained in:
2026-02-19 11:47:47 -05:00
parent 33dd49e20a
commit ca4adbcebf
30 changed files with 309 additions and 591 deletions
+2 -4
View File
@@ -56,8 +56,7 @@ __all__ = [
@on_setup
async def setup(ctx: ModuleContext) -> None:
"""
Initialize the timers module.
"""Initialize the timers module.
Creates the database schema, initializes chat counters for enabled timers,
and starts the background scheduler.
@@ -97,8 +96,7 @@ async def setup(ctx: ModuleContext) -> None:
@on_teardown
async def teardown(ctx: ModuleContext) -> None:
"""
Clean up the timers module.
"""Clean up the timers module.
Stops the background scheduler task.
@@ -32,8 +32,7 @@ from .scheduler import get_scheduler
@on_event(EventType.CHAT, priority=Priority.LOWEST)
async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
"""
Count a chat message for all tracked timers.
"""Count a chat message for all tracked timers.
Runs at lowest priority so all other CHAT handlers (moderation, etc.)
execute first. Skips bot messages and hidden messages.
@@ -60,8 +59,7 @@ async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
@on_event(EventType.VISIBILITY_UPDATE, priority=Priority.LOWEST)
async def handle_visibility_update(ctx: EventContext[VisibilityUpdateEvent]) -> None:
"""
Remove hidden messages from chat counts.
"""Remove hidden messages from chat counts.
Only handles the hide case. Un-hiding does not re-add messages because
we cannot distinguish previously counted user messages from bot messages
+10 -20
View File
@@ -33,8 +33,7 @@ _NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,31}$")
async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row | None:
"""
Resolve a timer by numeric ID or name.
"""Resolve a timer by numeric ID or name.
Tries parsing as an integer first, then falls back to a name lookup.
@@ -66,8 +65,7 @@ async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row
def _timer_display(row: aiosqlite.Row) -> str:
"""
Format a timer's display identifier.
"""Format a timer's display identifier.
:param row: Timer database row.
:return: Display string like "timer_name (#3)" or "timer #3".
@@ -79,8 +77,7 @@ def _timer_display(row: aiosqlite.Row) -> str:
@on_command("addtimer", requires_moderator=True)
async def addtimer(ctx: CommandContext) -> None:
"""
Create a new empty timer with an optional name.
"""Create a new empty timer with an optional name.
Usage: !addtimer [name]
@@ -123,8 +120,7 @@ async def addtimer(ctx: CommandContext) -> None:
@on_command("settimermessage", requires_moderator=True)
async def settimermessage(ctx: CommandContext) -> None:
"""
Set the message text for a timer.
"""Set the message text for a timer.
Usage: !settimermessage <id|name> <message>
@@ -164,8 +160,7 @@ async def settimermessage(ctx: CommandContext) -> None:
@on_command("settimerinterval", requires_moderator=True)
async def settimerinterval(ctx: CommandContext) -> None:
"""
Set the interval for a timer.
"""Set the interval for a timer.
Accepts simple durations (15m, 1h30m) or cron expressions (*/15 * * * *).
@@ -212,8 +207,7 @@ async def settimerinterval(ctx: CommandContext) -> None:
@on_command("settimerlines", requires_moderator=True)
async def settimerlines(ctx: CommandContext) -> None:
"""
Set the minimum chat lines between timer firings.
"""Set the minimum chat lines between timer firings.
Usage: !settimerlines <id|name> <count>
@@ -260,8 +254,7 @@ async def settimerlines(ctx: CommandContext) -> None:
@on_command("enabletimer", requires_moderator=True)
async def enabletimer(ctx: CommandContext) -> None:
"""
Enable a timer.
"""Enable a timer.
Won't enable a timer that has no message set.
@@ -310,8 +303,7 @@ async def enabletimer(ctx: CommandContext) -> None:
@on_command("disabletimer", requires_moderator=True)
async def disabletimer(ctx: CommandContext) -> None:
"""
Disable a timer.
"""Disable a timer.
Usage: !disabletimer <id|name>
@@ -351,8 +343,7 @@ async def disabletimer(ctx: CommandContext) -> None:
@on_command("deletetimer", requires_moderator=True)
async def deletetimer(ctx: CommandContext) -> None:
"""
Permanently delete a timer.
"""Permanently delete a timer.
Usage: !deletetimer <id|name>
@@ -382,8 +373,7 @@ async def deletetimer(ctx: CommandContext) -> None:
@on_command("listtimers", requires_moderator=True, cooldown=15)
async def listtimers(ctx: CommandContext) -> None:
"""
Send the URL to the timer list web page.
"""Send the URL to the timer list web page.
Usage: !listtimers
+1 -2
View File
@@ -31,8 +31,7 @@ _jinja_env = jinja2.Environment(
@on_route("/list", methods=["GET"])
async def timer_list_page(ctx: RouteContext) -> web.Response:
"""
Serve an HTML page listing all timers in a table.
"""Serve an HTML page listing all timers in a table.
Columns: #, Name, Message, Interval, Min Lines, Status, Last Fired.
Accessible at /owlbot/timers/list.
+16 -30
View File
@@ -51,8 +51,7 @@ _MIN_CRON_MINUTES = 1
def parse_interval(value: str) -> tuple[IntervalType, str]:
"""
Parse and validate an interval string.
"""Parse and validate an interval string.
Simple durations (no spaces) are parsed with a regex. Cron expressions
(contain spaces) are validated with cronsim. Returns the detected type
@@ -72,8 +71,7 @@ def parse_interval(value: str) -> tuple[IntervalType, str]:
def _parse_simple(value: str) -> tuple[IntervalType, str]:
"""
Parse and validate a simple duration string.
"""Parse and validate a simple duration string.
:param value: Duration string like "30s", "5m", "1h30m".
:return: Tuple of (IntervalType.SIMPLE, value).
@@ -99,8 +97,7 @@ def _parse_simple(value: str) -> tuple[IntervalType, str]:
def _parse_cron(value: str) -> tuple[IntervalType, str]:
"""
Validate a cron expression.
"""Validate a cron expression.
:param value: Cron expression string (5 fields).
:return: Tuple of (IntervalType.CRON, value).
@@ -125,8 +122,7 @@ def _parse_cron(value: str) -> tuple[IntervalType, str]:
def _duration_to_seconds(value: str) -> int:
"""
Convert a validated simple duration string to total seconds.
"""Convert a validated simple duration string to total seconds.
:param value: A previously validated duration string.
:return: Total seconds.
@@ -141,8 +137,7 @@ def _duration_to_seconds(value: str) -> int:
def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
"""
Compute when a timer will next be time-due.
"""Compute when a timer will next be time-due.
:param row: Database row with interval_type, interval_value, last_fired_at.
:param now: Current UTC time.
@@ -167,8 +162,7 @@ def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
def _is_timer_due(row: aiosqlite.Row, now: datetime) -> bool:
"""
Check whether a timer should fire based on its interval and last fire time.
"""Check whether a timer should fire based on its interval and last fire time.
:param row: Database row with interval_type, interval_value, last_fired_at.
:param now: Current UTC time.
@@ -187,6 +181,7 @@ class TimerScheduler:
"""
def __init__(self) -> None:
"""Initialize an empty scheduler with no running task."""
self._task: asyncio.Task[None] | None = None
self._counted_ids: dict[int, set[str]] = {}
self._wake_event: asyncio.Event | None = None
@@ -197,8 +192,7 @@ class TimerScheduler:
return self._counted_ids
def init_counted_ids(self, timer_ids: list[int]) -> None:
"""
Initialize empty counter sets for the given timer IDs.
"""Initialize empty counter sets for the given timer IDs.
Called during module setup to prepare tracking for enabled timers.
@@ -207,8 +201,7 @@ class TimerScheduler:
self._counted_ids = {tid: set() for tid in timer_ids}
def start(self, ctx: ModuleContext) -> None:
"""
Start the background scheduler task.
"""Start the background scheduler task.
:param ctx: The module context.
"""
@@ -219,8 +212,7 @@ class TimerScheduler:
ctx.logger.debug("Timer scheduler started.")
async def stop(self, ctx: ModuleContext) -> None:
"""
Cancel the background scheduler task and wait for it to exit.
"""Cancel the background scheduler task and wait for it to exit.
:param ctx: The module context.
"""
@@ -233,8 +225,7 @@ class TimerScheduler:
ctx.logger.info("Timer scheduler stopped.")
def reschedule(self) -> None:
"""
Wake the scheduler so it recalculates the next fire time.
"""Wake the scheduler so it recalculates the next fire time.
Called by timer management commands when timer state changes.
"""
@@ -242,8 +233,7 @@ class TimerScheduler:
self._wake_event.set()
async def _scheduler_loop(self, ctx: ModuleContext, wake: asyncio.Event) -> None:
"""
Background loop that sleeps until the next timer is due and fires it.
"""Background loop that sleeps until the next timer is due and fires it.
Uses an asyncio.Event to allow early wake-ups when timer state changes.
@@ -283,8 +273,7 @@ class TimerScheduler:
ctx.logger.exception("Scheduler tick failed.")
async def _tick(self, ctx: ModuleContext) -> None:
"""
Single scheduler tick: query enabled timers and fire any that are due.
"""Single scheduler tick: query enabled timers and fire any that are due.
:param ctx: The module context.
"""
@@ -342,8 +331,7 @@ class TimerScheduler:
async def _compute_next_delay(ctx: ModuleContext) -> tuple[float | None, str | None]:
"""
Query enabled timers and return seconds until the soonest one is time-due.
"""Query enabled timers and return seconds until the soonest one is time-due.
:param ctx: The module context.
:return: Tuple of (seconds until next timer, display name of that timer),
@@ -394,8 +382,7 @@ _scheduler: TimerScheduler | None = None
def get_scheduler() -> TimerScheduler:
"""
Return the active scheduler instance.
"""Return the active scheduler instance.
:return: The active TimerScheduler.
:raises RuntimeError: If the scheduler has not been initialized.
@@ -406,8 +393,7 @@ def get_scheduler() -> TimerScheduler:
def set_scheduler(scheduler: TimerScheduler) -> None:
"""
Set the active scheduler instance.
"""Set the active scheduler instance.
:param scheduler: The TimerScheduler to install.
"""