From 5b56a1d1c811e427a7d5dc2b39e5c484345d63b7 Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Tue, 28 Apr 2026 00:36:11 -0400 Subject: [PATCH 1/4] Update to add ty support, fixed ty issues on main code. --- src/api/endpoints.py | 19 +++++++------ src/config.py | 25 +++++++++++------ src/core/cshcalendar.py | 42 ++++++++++++++++------------ src/core/slack.py | 32 +++++++++++---------- src/core/wikithoughts.py | 54 ++++++++++++++++++++++++++---------- src/main.py | 14 ++++------ tests/conftest.py | 3 +- tests/src/core/test_slack.py | 2 +- tests/src/test_config.py | 2 +- ty.toml | 2 ++ 10 files changed, 122 insertions(+), 73 deletions(-) create mode 100644 ty.toml diff --git a/src/api/endpoints.py b/src/api/endpoints.py index b5985a6..5166301 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -1,13 +1,12 @@ -from logging import getLogger, Logger - import json -import httpx +from logging import Logger, getLogger -from fastapi import APIRouter, Request, Form +import httpx +from fastapi import APIRouter, Form, Request from fastapi.responses import JSONResponse -from core import slack, wikithoughts, cshcalendar from config import WATCHED_CHANNELS +from core import cshcalendar, slack, wikithoughts logger: Logger = getLogger(__name__) router: APIRouter = APIRouter() @@ -28,9 +27,13 @@ async def get_calendar() -> JSONResponse: events: list[dict[str, str]] = [] try: - get_future_events_ical: list[ - cshcalendar.CalendarInfo - ] = await cshcalendar.get_future_events() + get_future_events_ical: ( + list[cshcalendar.CalendarInfo] | None + ) = await cshcalendar.get_future_events() + + if get_future_events_ical is None: + raise Exception("Gathering future events resulted in None") + events = cshcalendar.format_events(get_future_events_ical) except Exception as e: logger.error(f"Error fetching calendar events: {e}") diff --git a/src/config.py b/src/config.py index 3e3eb81..ea26991 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,8 @@ -import os import json import logging +import os +from typing import overload + from dotenv import load_dotenv load_dotenv() @@ -8,6 +10,14 @@ logger: logging.Logger = logging.getLogger(__name__) +@overload +def _get_env_variable(name: str, default: None = None) -> str | None: ... + + +@overload +def _get_env_variable(name: str, default: str) -> str: ... + + def _get_env_variable(name: str, default: str | None = None) -> str | None: """ Retrieves an environment variable, with an optional default value. @@ -21,7 +31,7 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: """ try: - value: str = os.getenv(name, default) + value: str | None = os.getenv(name, default) if value in (None, ""): logger.warning( @@ -39,10 +49,9 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: SLACK_API_TOKEN: str | None = _get_env_variable("SLACK_API_TOKEN", None) SLACK_JUMPSTART_MESSAGE: str = "Would you like to post this message to Jumpstart?" -WATCHED_CHANNELS: tuple[str] = tuple( - _get_env_variable("WATCHED_CHANNELS", "").split(",") -) -SLACK_DM_TEMPLATE: dict | None = None +RAW_CHANNELS: str = _get_env_variable("WATCHED_CHANNELS", "") +WATCHED_CHANNELS: tuple[str, ...] = tuple(RAW_CHANNELS.split(",")) +SLACK_DM_TEMPLATE: list | None = None CALENDAR_URL: str | None = _get_env_variable("CALENDAR_URL", None) CALENDAR_OUTLOOK_DAYS: int = int(_get_env_variable("CALENDAR_OUTLOOK_DAYS", "7")) @@ -51,8 +60,8 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: CALENDAR_CACHE_REFRESH: int = int(_get_env_variable("CALENDAR_CACHE_REFRESH", "10")) WIKI_API: str | None = _get_env_variable("WIKI_API", None) -WIKIBOT_USER: str | None = _get_env_variable("WIKIBOT_USER", None) -WIKIBOT_PASSWORD: str | None = _get_env_variable("WIKIBOT_PASSWORD", None) +WIKIBOT_USER: str = _get_env_variable("WIKIBOT_USER", "") +WIKIBOT_PASSWORD: str = _get_env_variable("WIKIBOT_PASSWORD", "") WIKI_CATEGORY: str = _get_env_variable("WIKI_CATEGORY", "JobAdvice") with open(os.path.join(BASE_DIR, "static", "slack", "dm_request_template.json")) as f: diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 01b7c34..7edc5ac 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -1,12 +1,13 @@ -from logging import getLogger, Logger -from datetime import datetime, date, timedelta, time +import asyncio +import re +from datetime import date, datetime, time, timedelta +from logging import Logger, getLogger from zoneinfo import ZoneInfo -from icalendar.cal import Event, Calendar +import arrow import httpx import recurring_ical_events -import arrow -import re +from icalendar.cal import Calendar, Event from config import ( CALENDAR_CACHE_REFRESH, @@ -15,7 +16,6 @@ CALENDAR_TIMEZONE, CALENDAR_URL, ) -import asyncio calendar_cache: list[CalendarInfo] = [] # The current cache of the calendar cal_last_update: date | None = ( @@ -45,7 +45,7 @@ WARNING: PERCENTAGE SIGNS WILL TRIGGER A REGEX OPERATION WARNING: FOLLOW INSERTION ORDER """ -HUMANIZER_CHECKS: dict[int, str] = { +HUMANIZER_CHECKS: dict[int | float, str] = { MINUTE: "In 1 Minute", (HOUR - MINUTE): f"In %{MINUTE}% Minutes", (HOUR * 1.5): "In 1 Hour", @@ -67,7 +67,7 @@ class CalendarInfo: def __init__(self, name: str, date_time: date, location: str | None = None): self.name: str = name - self.date: arrow.arrow = arrow.get(date_time) # Arrow has way cooler stuff + self.date: arrow.Arrow = arrow.get(date_time) # Arrow has way cooler stuff self.location: str | None = location def __eq__(self, other): @@ -93,7 +93,7 @@ def ceil_division(num: int, den: int) -> int: return (num + den - 1) // den -def time_humanizer(current_time: datetime, event_time: datetime) -> str: +def time_humanizer(current_time: datetime, event_time: arrow.Arrow) -> str: """ Custom humanizer for text to be displayed @@ -118,7 +118,7 @@ def repl(match: re.Match[str]) -> str: num = int(match.group(1)) return str(round(time_before_event / num)) - time_before_event: int = (event_time - current_time).total_seconds() + time_before_event: int | float = (event_time - current_time).total_seconds() if time_before_event > WEEK: return "Over a Week Away" @@ -136,6 +136,7 @@ def repl(match: re.Match[str]) -> str: return TIME_PATTERN.sub(repl, unformatted_string) + def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]: """ Formats a parsed list of CalendarInfos, and returns the HTML required for front end @@ -150,7 +151,7 @@ def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]: current_date: date = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) if not events: - return {"data": [{"header": ":(", "content": "No Events on the Calendar"}]} + return [{"header": ":(", "content": "No Events on the Calendar"}] formatted_list: list[dict[str, str]] = [] @@ -181,6 +182,9 @@ async def rebuild_calendar() -> None: global calendar_cache, cal_last_update, cal_constructed_event + if CALENDAR_URL is None: + raise Exception("Calendar URL is None, cant request.") + current_time: datetime = datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) try: cal_constructed_event.clear() @@ -188,11 +192,11 @@ async def rebuild_calendar() -> None: response: httpx.Response = await cshcal_client.get(CALENDAR_URL, timeout=10) response.raise_for_status() - cal: Calendar = Calendar.from_ical(response.content) + cal: Calendar | None = Calendar.from_ical(response.content) - fetched_daily_events: list[Event] = recurring_ical_events.of(cal).between( - current_time, current_time + timedelta(days=CALENDAR_OUTLOOK_DAYS) - ) + fetched_daily_events: list[Event] | None = recurring_ical_events.of( + cal + ).between(current_time, current_time + timedelta(days=CALENDAR_OUTLOOK_DAYS)) for event in fetched_daily_events: dt = event.get("DTSTART").dt @@ -228,7 +232,7 @@ async def rebuild_calendar() -> None: cal_constructed_event.set() -async def get_future_events() -> list[CalendarInfo]: +async def get_future_events() -> list[CalendarInfo] | None: """ Returns the first events up to event maximum within the the calendar outlook day amount custom object has name, date and the location @@ -244,6 +248,9 @@ async def get_future_events() -> list[CalendarInfo]: header_none_match, \ cal_constructed_event + if CALENDAR_URL is None: + raise Exception("Calendar URL is None, cant request.") + if not cal_constructed_event.is_set(): await cal_constructed_event.wait() return calendar_cache @@ -261,10 +268,11 @@ async def get_future_events() -> list[CalendarInfo]: logger.info("Checking to rebuild CSH Calendar...") try: - headers: dict[str, str | None] = {} + headers: dict[str, str] = {} if header_none_match: headers["If-None-Match"] = header_none_match + if header_last_modified: headers["If-Modified-Since"] = header_last_modified diff --git a/src/core/slack.py b/src/core/slack.py index 1b410b3..8dfa91f 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -1,21 +1,20 @@ -import re import copy import json +import re +from datetime import datetime +from logging import Logger, getLogger +from zoneinfo import ZoneInfo -from logging import getLogger, Logger - -from slack_sdk.web.async_client import AsyncWebClient -from slack_sdk.web.slack_response import SlackResponse from slack_sdk.errors import SlackApiError +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.async_slack_response import AsyncSlackResponse from config import ( + CALENDAR_TIMEZONE, SLACK_API_TOKEN, - SLACK_JUMPSTART_MESSAGE, SLACK_DM_TEMPLATE, - CALENDAR_TIMEZONE, + SLACK_JUMPSTART_MESSAGE, ) -from datetime import datetime -from zoneinfo import ZoneInfo logger: Logger = getLogger(__name__) @@ -32,11 +31,10 @@ "user": "Jumpstart", "timestamp": datetime.now(ZoneInfo(CALENDAR_TIMEZONE)) .strftime("%I:%M %p") - .lstrip("0") + .lstrip("0"), } - def clean_text(raw: str) -> str: """ Strip Slack mrkdwn, HTML entities, and formatting characters. @@ -67,7 +65,7 @@ async def gather_emojis() -> dict: if client is None: raise ValueError("Slack client is not initialized") - emoji_request: dict = await client.emoji_list() + emoji_request: AsyncSlackResponse = await client.emoji_list() assert emoji_request.get("ok", False) emojis = emoji_request.get("emoji", {}) @@ -88,7 +86,10 @@ async def get_username(user_id: str) -> str: str: The username, or an empty string if not applicable """ - response = await client.users_info(user=user_id) + if client is None: + raise ValueError("Slack client is not initialized") + + response: AsyncSlackResponse = await client.users_info(user=user_id) user = response.get("user", None) if user is None: @@ -115,7 +116,10 @@ async def request_upload_via_dm(user_id: str, announcement_text: str) -> None: if client is None: raise ValueError("Slack client is not initialized") - message: dict = copy.deepcopy(SLACK_DM_TEMPLATE) + message: list | None = copy.deepcopy(SLACK_DM_TEMPLATE) + + if message is None: + raise Exception("Unable to deepcopy dm template.") message[0]["text"]["text"] += announcement_text message[1]["elements"][0]["value"] = json.dumps( diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 1a3e715..77ce73e 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -1,15 +1,14 @@ -import re -import httpx -import random import asyncio import logging - -from typing import Pattern -from itertools import islice +import random +import re from datetime import datetime, timedelta +from itertools import islice +from typing import Pattern -from config import WIKIBOT_PASSWORD, WIKIBOT_USER, WIKI_CATEGORY, WIKI_API +import httpx +from config import WIKI_API, WIKI_CATEGORY, WIKIBOT_PASSWORD, WIKIBOT_USER CYCLE_DEBOUNCE_TIME: int = 12 # How long it takes to resfresh wiki titles BATCH_SIZE: int = 50 # max titles per request @@ -17,15 +16,15 @@ 3 # The amount of times it will attempt to re-authenticare ) -HEADERS: dict[str, str] = {"User-Agent": "JumpstartFetcher/1.0"} -AUTH: tuple[str] = (WIKIBOT_USER, WIKIBOT_PASSWORD) +_HEADERS: dict[str, str] = {"User-Agent": "JumpstartFetcher/1.0"} +_AUTH: tuple[str, str] = (WIKIBOT_USER, WIKIBOT_PASSWORD) logger: logging.Logger = logging.getLogger(__name__) client: httpx.AsyncClient | None = None try: - client = httpx.AsyncClient(headers=HEADERS, auth=AUTH) + client = httpx.AsyncClient(headers=_HEADERS, auth=_AUTH) except Exception as e: logger.warning(f"Failed to initialize HTTP client for wiki: {e}") @@ -68,7 +67,7 @@ def clean_wikitext(text: str) -> str: str: The cleaned up text string """ - reg_operations: tuple[Pattern[str]] = ( + reg_operations: tuple[Pattern[str], ...] = ( RE_FILE, RE_IMAGE, RE_LINK, @@ -218,12 +217,12 @@ def needs_category_refresh(update_time: datetime) -> bool: ) -def process_category_page(r_json: dict[str, str]) -> tuple[list[str], bool | str]: +def process_category_page(r_json: dict) -> tuple[list[str], bool | str]: """ Processes a wikithoughts response into a list of title pages Args: - r_json (dict[str,str]): The JSON from the wiki to be processed + r_json: The JSON from the wiki to be processed Returns: tuple[list[str], bool | str]: The list of titles from the request, along with a possible continutation if needed @@ -256,7 +255,19 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: list[str]: The list of titles to be fetched. """ - params: dict[str, str] = { + if not client: + logger.warning( + "HTTP client for wiki is not initialized, unable to fetch category pages!" + ) + return [] + + if not WIKI_API: + logger.warning( + "There is no WIKI_API set to make requests, unable to fetch category pages!" + ) + return [] + + params: dict[str, str | bool] = { "action": "query", "list": "categorymembers", "cmtitle": f"Category:{WIKI_CATEGORY}", @@ -269,7 +280,7 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: failed_authentication_attempts: int = 0 while True: - r_json: dict[str, str] = response.json() + r_json: dict = response.json() if "error" in r_json and r_json["error"].get("code") in ( "readapidenied", @@ -325,7 +336,14 @@ async def refresh_category_pages() -> list[str]: ) return [] + if not WIKI_API: + logger.warning( + "There is no WIKI_API set to make requests, unable to refresh category pages!" + ) + return [] + global page_title_cache, last_updated_time, queued_pages, shown_pages + time_now: datetime = datetime.now() if not needs_category_refresh(time_now): @@ -378,6 +396,12 @@ async def refresh_page_dictionary() -> None: ) return + if not WIKI_API: + logger.warning( + "There is no WIKI_API set to make requests, unable to refresh page dictionary!" + ) + return + global page_dict_cache, page_title_cache if not page_title_cache: diff --git a/src/main.py b/src/main.py index 0fd0da7..a131c1b 100644 --- a/src/main.py +++ b/src/main.py @@ -5,21 +5,19 @@ V1 Authors: Beckett Jenen """ -import os import asyncio - -from logging import getLogger, Logger +import os +from contextlib import asynccontextmanager +from logging import Logger, getLogger from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from fastapi.responses import RedirectResponse, HTMLResponse -from contextlib import asynccontextmanager - -from config import BASE_DIR from api import endpoints -from core import wikithoughts, cshcalendar +from config import BASE_DIR +from core import cshcalendar, wikithoughts logger: Logger = getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index 96dcc6a..cd8997c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ -import coverage import os +import coverage + # Get the absolute path to the src directory src_path = os.path.join(os.path.dirname((os.path.abspath(__file__))), "..", "src") diff --git a/tests/src/core/test_slack.py b/tests/src/core/test_slack.py index ea952a4..015cfda 100644 --- a/tests/src/core/test_slack.py +++ b/tests/src/core/test_slack.py @@ -1,6 +1,6 @@ -import sys import asyncio import importlib +import sys def import_slack_module(monkeypatch) -> object: diff --git a/tests/src/test_config.py b/tests/src/test_config.py index 767b36f..b1aa955 100644 --- a/tests/src/test_config.py +++ b/tests/src/test_config.py @@ -1,5 +1,5 @@ -import sys import importlib +import sys def import_config_module() -> object: diff --git a/ty.toml b/ty.toml new file mode 100644 index 0000000..868b754 --- /dev/null +++ b/ty.toml @@ -0,0 +1,2 @@ +[environment] +extra-paths = ["./src"] From 796942c0eef29befeb1623f4da7a4de5fa7f9ccc Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Thu, 30 Apr 2026 21:14:08 -0400 Subject: [PATCH 2/4] Add logging levels --- .env.template | 2 ++ docker-compose.yml | 3 ++- mkdocs.yml | 2 +- src/api/endpoints.py | 6 +++--- src/config.py | 18 ++++++++++++++++-- src/core/cshcalendar.py | 25 +++++++++++-------------- src/core/slack.py | 5 +++-- src/core/wikithoughts.py | 25 +++++++++++++++---------- src/main.py | 3 ++- 9 files changed, 55 insertions(+), 34 deletions(-) diff --git a/.env.template b/.env.template index 853032e..edcbb45 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,5 @@ +LOGGING_LEVEL=DEBUG + CALENDAR_URL= CALENDAR_OUTLOOK_DAYS= CALENDAR_EVENT_MAXIMUM= diff --git a/docker-compose.yml b/docker-compose.yml index 6468237..0d6f13f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ --- - services: jumpstart: build: . @@ -7,6 +6,8 @@ services: ports: - "8000:8000" environment: + - LOGGING_LEVEL=${LOGGING_LEVEL} + - CALENDAR_URL=${CALENDAR_URL} - CALENDAR_OUTLOOK_DAYS=${CALENDAR_OUTLOOK_DAYS} - CALENDAR_EVENT_MAXIMUM=${CALENDAR_EVENT_MAXIMUM} diff --git a/mkdocs.yml b/mkdocs.yml index 1b7673e..a806442 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,7 +38,7 @@ use_directory_urls: false nav: - Home: index.md - Getting Started: getting-started/getting-started.md - - Backend: + - Backend: - Calendar: core/csh_calendar.md - Slack: core/slack.md - Wikithoughts: core/wikithoughts.md diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 5166301..634f1b8 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -34,7 +34,7 @@ async def get_calendar() -> JSONResponse: if get_future_events_ical is None: raise Exception("Gathering future events resulted in None") - events = cshcalendar.format_events(get_future_events_ical) + events.extend(cshcalendar.format_events(get_future_events_ical)) except Exception as e: logger.error(f"Error fetching calendar events: {e}") return JSONResponse({"status": "error", "message": str(e)}, status_code=500) @@ -126,8 +126,8 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: "User approved the announcement, Adding it to the announcement list!" ) - message_object: dict[str, dict] = json.loads( - form_json.get("actions", [{}])[0].get("value", '{text:""}') + message_object: str | None = json.loads( + form_json.get("actions", [{}])[0].get("value", {}) ).get("text", None) user_id = form_json.get("user", {}).get("id") diff --git a/src/config.py b/src/config.py index ea26991..92cc15e 100644 --- a/src/config.py +++ b/src/config.py @@ -47,10 +47,23 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: BASE_DIR: str = os.path.dirname(os.path.abspath(__file__)) +_raw_logging_level: str = _get_env_variable("LOGGING_LEVEL", "DEBUG") +LOGGING_LEVEL: int = logging.INFO + +match _raw_logging_level: + case "DEBUG": + LOGGING_LEVEL = logging.DEBUG + case "WARN": + LOGGING_LEVEL = logging.WARN + SLACK_API_TOKEN: str | None = _get_env_variable("SLACK_API_TOKEN", None) SLACK_JUMPSTART_MESSAGE: str = "Would you like to post this message to Jumpstart?" RAW_CHANNELS: str = _get_env_variable("WATCHED_CHANNELS", "") WATCHED_CHANNELS: tuple[str, ...] = tuple(RAW_CHANNELS.split(",")) + +SLACK_DM_TEMPLATE_FILEPATH: str = os.path.join( + BASE_DIR, "static", "slack", "dm_request_template.json" +) SLACK_DM_TEMPLATE: list | None = None CALENDAR_URL: str | None = _get_env_variable("CALENDAR_URL", None) @@ -64,5 +77,6 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: WIKIBOT_PASSWORD: str = _get_env_variable("WIKIBOT_PASSWORD", "") WIKI_CATEGORY: str = _get_env_variable("WIKI_CATEGORY", "JobAdvice") -with open(os.path.join(BASE_DIR, "static", "slack", "dm_request_template.json")) as f: - SLACK_DM_TEMPLATE = json.load(f) +if os.path.exists(SLACK_DM_TEMPLATE_FILEPATH): + with open(SLACK_DM_TEMPLATE_FILEPATH, mode="r") as f: + SLACK_DM_TEMPLATE = json.load(f) diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index 7edc5ac..fe1134b 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -15,6 +15,7 @@ CALENDAR_OUTLOOK_DAYS, CALENDAR_TIMEZONE, CALENDAR_URL, + LOGGING_LEVEL, ) calendar_cache: list[CalendarInfo] = [] # The current cache of the calendar @@ -31,6 +32,8 @@ cal_constructed_event.clear() logger: Logger = getLogger(__name__) +logger.setLevel(LOGGING_LEVEL) + logger.info("Starting up the calendar service!") cshcal_client = httpx.AsyncClient() @@ -157,19 +160,16 @@ def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]: for event in events: content_dict: dict[str, str] = {} + content_dict["content"] = str(event.name) - event_cur_happening: bool = event.date < current_date - if event_cur_happening: - formatted: str = ( + if event.date < current_date: + content_dict["header"] = ( f"Happening in {event.location}!" if event.location else "Happening Now!" ) - content_dict["header"] = formatted - content_dict["content"] = str(event.name) else: content_dict["header"] = time_humanizer(current_date, event.date) - content_dict["content"] = str(event.name) formatted_list.append(content_dict) return formatted_list @@ -194,19 +194,17 @@ async def rebuild_calendar() -> None: cal: Calendar | None = Calendar.from_ical(response.content) - fetched_daily_events: list[Event] | None = recurring_ical_events.of( - cal - ).between(current_time, current_time + timedelta(days=CALENDAR_OUTLOOK_DAYS)) + fetched_daily_events: list[Event] = recurring_ical_events.of(cal).between( + current_time, current_time + timedelta(days=CALENDAR_OUTLOOK_DAYS) + ) for event in fetched_daily_events: dt = event.get("DTSTART").dt if isinstance(dt, date) and not isinstance(dt, datetime): dt = datetime.combine(dt, time.min, tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) - elif dt.tzinfo is None: dt = dt.replace(tzinfo=ZoneInfo(CALENDAR_TIMEZONE)) - else: dt = dt.astimezone(ZoneInfo(CALENDAR_TIMEZONE)) @@ -218,11 +216,10 @@ async def rebuild_calendar() -> None: found_events.add(new_event) - cal = None - fetched_daily_events = None + fetched_daily_events.clear() except Exception as e: logger.warning("Failed to rebuild calendar cache! Error:") - logger.warning(e) + logger.error(e) cal_constructed_event.set() cal_last_update = current_time diff --git a/src/core/slack.py b/src/core/slack.py index 8dfa91f..f75f931 100644 --- a/src/core/slack.py +++ b/src/core/slack.py @@ -11,13 +11,14 @@ from config import ( CALENDAR_TIMEZONE, + LOGGING_LEVEL, SLACK_API_TOKEN, SLACK_DM_TEMPLATE, SLACK_JUMPSTART_MESSAGE, ) logger: Logger = getLogger(__name__) - +logger.setLevel(LOGGING_LEVEL) client: AsyncWebClient | None = None @@ -173,7 +174,7 @@ def get_announcement() -> dict[str, str] | None: return current_announcement -def add_announcement(announcement_text: str, username: str) -> None: +def add_announcement(announcement_text: str | None, username: str) -> None: """ Adds an announcement to the queue. diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 77ce73e..526c7ba 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -8,7 +8,13 @@ import httpx -from config import WIKI_API, WIKI_CATEGORY, WIKIBOT_PASSWORD, WIKIBOT_USER +from config import ( + LOGGING_LEVEL, + WIKI_API, + WIKI_CATEGORY, + WIKIBOT_PASSWORD, + WIKIBOT_USER, +) CYCLE_DEBOUNCE_TIME: int = 12 # How long it takes to resfresh wiki titles BATCH_SIZE: int = 50 # max titles per request @@ -20,6 +26,7 @@ _AUTH: tuple[str, str] = (WIKIBOT_USER, WIKIBOT_PASSWORD) logger: logging.Logger = logging.getLogger(__name__) +logger.setLevel(LOGGING_LEVEL) client: httpx.AsyncClient | None = None @@ -132,6 +139,8 @@ async def auth_bot() -> None: ) return + global bot_authenticated + token_req: httpx.Response = await client.get( WIKI_API, params={"action": "query", "meta": "tokens", "type": "login", "format": "json"}, @@ -153,8 +162,6 @@ async def auth_bot() -> None: returned_json: dict = login_req.json()["login"] if returned_json and returned_json["result"] == "Success": - global bot_authenticated - bot_authenticated = True logger.info("Bot was authenticated successfully!") else: @@ -237,11 +244,10 @@ def process_category_page(r_json: dict) -> tuple[list[str], bool | str]: # Loop to keep everything going if "continue" in r_json: return (titles, r_json["continue"]["cmcontinue"]) - - return (titles, False) else: logger.warning(f"Failure in obtaining info, JSON:\n{r_json}") - return (titles, False) + + return (titles, False) async def fetch_category_pages(response: httpx.Response) -> list[str]: @@ -307,7 +313,7 @@ async def fetch_category_pages(response: httpx.Response) -> list[str]: continue added, repeat_req = process_category_page(r_json) - titles_found += added + titles_found.extend(added) if repeat_req not in (None, False, ""): params["cmcontinue"] = repeat_req @@ -349,7 +355,6 @@ async def refresh_category_pages() -> list[str]: if not needs_category_refresh(time_now): return page_title_cache - titles: list[str] = [] params: dict[str, str] = { "action": "query", "list": "categorymembers", @@ -379,7 +384,7 @@ async def refresh_category_pages() -> list[str]: queued_pages = titles.copy() random.shuffle(queued_pages) - shown_pages = [] + shown_pages.clear() await refresh_page_dictionary() return page_title_cache @@ -459,7 +464,7 @@ def reset_queues() -> None: queued_pages = shown_pages random.shuffle(queued_pages) - shown_pages = [] + shown_pages.clear() async def get_next_display() -> dict[str, str]: diff --git a/src/main.py b/src/main.py index a131c1b..a74cc65 100644 --- a/src/main.py +++ b/src/main.py @@ -16,10 +16,11 @@ from fastapi.templating import Jinja2Templates from api import endpoints -from config import BASE_DIR +from config import BASE_DIR, LOGGING_LEVEL from core import cshcalendar, wikithoughts logger: Logger = getLogger(__name__) +logger.setLevel(LOGGING_LEVEL) @asynccontextmanager From cc10c7ee89b43dde75f94d53e51cdc55092c40a1 Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Sat, 2 May 2026 17:51:19 -0400 Subject: [PATCH 3/4] Simplify some --- src/api/endpoints.py | 24 ++++++++++-------------- src/core/cshcalendar.py | 3 +-- src/core/wikithoughts.py | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 634f1b8..2b6d037 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -121,6 +121,8 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: if form_json.get("type") != "block_actions": return JSONResponse({}, status_code=200) + message: str = DENY_MESSAGE + if slack.convert_user_response_to_bool(form_json): logger.info( "User approved the announcement, Adding it to the announcement list!" @@ -136,20 +138,14 @@ async def message_actions(payload: str = Form(...)) -> JSONResponse: username = username[:40] slack.add_announcement(message_object, username) - - if response_url: - async with httpx.AsyncClient() as client: - await client.post( - response_url, - json={"text": ACCEPT_MESSAGE, "replace_original": True}, - ) - else: - if response_url: - async with httpx.AsyncClient() as client: - await client.post( - response_url, - json={"text": DENY_MESSAGE, "replace_original": True}, - ) + message: str = ACCEPT_MESSAGE + + if response_url: + async with httpx.AsyncClient() as client: + await client.post( + response_url, + json={"text": message, "replace_original": True}, + ) except Exception as e: logger.error(f"Error in message_actions: {e}") diff --git a/src/core/cshcalendar.py b/src/core/cshcalendar.py index fe1134b..bb5ec47 100644 --- a/src/core/cshcalendar.py +++ b/src/core/cshcalendar.py @@ -159,8 +159,7 @@ def format_events(events: list[CalendarInfo]) -> list[dict[str, str]]: formatted_list: list[dict[str, str]] = [] for event in events: - content_dict: dict[str, str] = {} - content_dict["content"] = str(event.name) + content_dict: dict[str, str] = {"content": str(event.name)} if event.date < current_date: content_dict["header"] = ( diff --git a/src/core/wikithoughts.py b/src/core/wikithoughts.py index 526c7ba..4bfe234 100644 --- a/src/core/wikithoughts.py +++ b/src/core/wikithoughts.py @@ -415,7 +415,7 @@ async def refresh_page_dictionary() -> None: results: dict[str, str] = {} tasks: list = [] for batch in batch_iterable(page_title_cache, BATCH_SIZE): - params = { + params: dict[str, str | bool] = { "action": "query", "prop": "revisions", "rvprop": "content", From cbba0cff55dcd69e41803a3f2fe09f9c71251212 Mon Sep 17 00:00:00 2001 From: Will Hellinger Date: Sat, 2 May 2026 18:38:40 -0400 Subject: [PATCH 4/4] add more logging levels --- src/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/config.py b/src/config.py index 92cc15e..a00a714 100644 --- a/src/config.py +++ b/src/config.py @@ -55,6 +55,12 @@ def _get_env_variable(name: str, default: str | None = None) -> str | None: LOGGING_LEVEL = logging.DEBUG case "WARN": LOGGING_LEVEL = logging.WARN + case "ERROR": + LOGGING_LEVEL = logging.ERROR + case "FATAL": + LOGGING_LEVEL = logging.FATAL + case "CRITICAL": + LOGGING_LEVEL = logging.CRITICAL SLACK_API_TOKEN: str | None = _get_env_variable("SLACK_API_TOKEN", None) SLACK_JUMPSTART_MESSAGE: str = "Would you like to post this message to Jumpstart?"