aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar kwzrd <[email protected]>2021-03-13 17:58:25 +0100
committerGravatar kwzrd <[email protected]>2021-03-13 18:41:17 +0100
commit81e48983c7408e6a8dd4c6131eb5633be7c53825 (patch)
tree2cddaca593a6727de34735fd56a32dd4e6c0b507
parentBranding: extract duration string in helper function (diff)
Branding: cache all available events
This is a prequel to adding a calendar command. To avoid re-querying the branding repo on command invocation, event information will be cached whenever we make requests. The command can then simply get an up-to-date event schedule from the cache, with the option of forcing an update via the 'populate_cache_events' function. Since we cannot easily serialize entire 'Event' instances, we simply store what's needed - the event name, and its duration. The author has verified that the cache maintains order; in this case chronological order based on event start date.
-rw-r--r--bot/exts/backend/branding/_cog.py49
-rw-r--r--bot/exts/backend/branding/_repository.py18
2 files changed, 56 insertions, 11 deletions
diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py
index 332d4ad58..50ae11b11 100644
--- a/bot/exts/backend/branding/_cog.py
+++ b/bot/exts/backend/branding/_cog.py
@@ -62,6 +62,18 @@ def extract_event_duration(event: Event) -> str:
return f"{start_date} - {end_date}"
+def extract_event_name(event: Event) -> str:
+ """
+ Extract title-cased event name from the path of `event`.
+
+ An event with a path of 'events/black_history_month' will resolve to 'Black History Month'.
+ """
+ name = event.path.split("/")[-1] # Inner-most directory name
+ words = name.split("_") # Words from snake case
+
+ return " ".join(word.title() for word in words)
+
+
class Branding(commands.Cog):
"""Guild branding management."""
@@ -80,6 +92,10 @@ class Branding(commands.Cog):
# corresponding to the amount of times each icon has been used in the current rotation
cache_icons = RedisCache()
+ # Cache holding all available event names & their durations; this is cached by the daemon and read by
+ # the calendar command with the intention of preventing API spam; doesn't contain the fallback event
+ cache_events = RedisCache()
+
def __init__(self, bot: Bot) -> None:
"""Instantiate repository abstraction & allow daemon to start."""
self.bot = bot
@@ -271,12 +287,35 @@ class Branding(commands.Cog):
"""
log.debug("Synchronise: fetching current event")
- event = await self.repository.get_current_event()
+ current_event, available_events = await self.repository.get_current_event()
- if event is None:
+ await self.populate_cache_events(available_events)
+
+ if current_event is None:
log.error("Failed to fetch event ~ cannot synchronise!")
else:
- await self.enter_event(event)
+ await self.enter_event(current_event)
+
+ async def populate_cache_events(self, events: t.List[Event]) -> None:
+ """
+ Clear `cache_events` and re-populate with names and durations of `events`.
+
+ For each event, we store its name and duration string. This is the information presented to users in the
+ calendar command. If a format change is needed, it has to be done here.
+
+ The cache does not store the fallback event, as it is not shown in the calendar.
+ """
+ log.debug(f"Populating events cache with {len(events)} events")
+
+ await self.cache_events.clear()
+
+ no_fallback = [event for event in events if not event.meta.is_fallback]
+ chronological_events = sorted(no_fallback, key=lambda event_: event_.meta.start_date)
+
+ await self.cache_events.update({
+ extract_event_name(event): extract_event_duration(event)
+ for event in chronological_events
+ })
# endregion
# region: Daemon
@@ -322,7 +361,9 @@ class Branding(commands.Cog):
"""
log.debug("Daemon awakens: checking current event")
- new_event = await self.repository.get_current_event()
+ new_event, available_events = await self.repository.get_current_event()
+
+ await self.populate_cache_events(available_events)
if new_event is None:
log.warning("Failed to get current event from the branding repository, daemon will do nothing!")
diff --git a/bot/exts/backend/branding/_repository.py b/bot/exts/backend/branding/_repository.py
index ef292619e..b1368c59e 100644
--- a/bot/exts/backend/branding/_repository.py
+++ b/bot/exts/backend/branding/_repository.py
@@ -189,11 +189,14 @@ class BrandingRepository:
log.trace(f"Found {len(instances)} correctly configured events")
return instances
- async def get_current_event(self) -> t.Optional[Event]:
+ async def get_current_event(self) -> t.Tuple[t.Optional[Event], t.List[Event]]:
"""
Get the currently active event, or the fallback event.
- Returns None in the case that no event is active, and no fallback event is found.
+ The second return value is a list of all available events. The caller may discard it, if not needed.
+ Returning all events alongside the current one prevents having to query the API twice in some cases.
+
+ The current event may be None in the case that no event is active, and no fallback event is found.
"""
utc_now = datetime.utcnow()
log.debug(f"Finding active event for: {utc_now}")
@@ -201,17 +204,18 @@ class BrandingRepository:
# As all events exist in the arbitrary year, we construct a separate object for the purposes of comparison
lookup_now = date(year=ARBITRARY_YEAR, month=utc_now.month, day=utc_now.day)
- events = await self.get_events()
+ available_events = await self.get_events()
- for event in events:
+ for event in available_events:
meta = event.meta
if not meta.is_fallback and (meta.start_date <= lookup_now <= meta.end_date):
- return event
+ return event, available_events
log.debug("No active event found, looking for fallback")
- for event in events:
+ for event in available_events:
if event.meta.is_fallback:
- return event
+ return event, available_events
log.warning("No event is currently active and no fallback event was found!")
+ return None, available_events