From 22454630f6414726069ac379a41bb7335982e55c Mon Sep 17 00:00:00 2001 From: Sacul0457Deve <183588943+Sacul0457@users.noreply.github.com.> Date: Sun, 2 Aug 2026 01:33:52 +0800 Subject: [PATCH 1/3] initial commit --- bot/exts/info/pep.py | 45 +++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index 002522d042..cbd8bb6b82 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -1,7 +1,8 @@ from datetime import UTC, datetime, timedelta from typing import TypedDict +from rapidfuzz import process -from discord import Colour, Embed +from discord import Colour, Embed, app_commands, Interaction from discord.ext.commands import Cog, Context, command from bot.bot import Bot @@ -13,6 +14,7 @@ ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" PEP_API_URL = "https://peps.python.org/api/peps.json" + class PEPInfo(TypedDict): """ Useful subset of the PEP API response. @@ -45,9 +47,7 @@ async def refresh_pep_data(self) -> None: log.trace("Started refreshing PEP data.") async with self.bot.http_session.get(PEP_API_URL) as resp: if resp.status != 200: - log.warning( - "Fetching PEP data from PEP API failed with code %s", - resp.status) + log.warning("Fetching PEP data from PEP API failed with code %s", resp.status) return listing = await resp.json() @@ -72,15 +72,9 @@ def generate_pep_embed(self, pep: PEPInfo) -> Embed: return embed - @command(name="pep", aliases=("get_pep", "p")) - async def pep_command(self, ctx: Context, pep_number: int) -> None: - """Fetches information about a PEP and sends it to the channel.""" - # Refresh the PEP data up to every hour, as e.g. the PEP status might have changed. - if ( - self.last_refreshed_peps is None or ( - (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC) - and len(str(pep_number)) < 5 - ) + async def get_pep_embed(self, pep_number: int) -> Embed: + if self.last_refreshed_peps is None or ( + (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC) and len(str(pep_number)) < 5 ): await self.refresh_pep_data() @@ -93,9 +87,34 @@ async def pep_command(self, ctx: Context, pep_number: int) -> None: description=f"PEP {pep_number} does not exist.", colour=Colour.red(), ) + return embed + @command(name="pep", aliases=("get_pep", "p")) + async def pep_command(self, ctx: Context, pep_number: int) -> None: + """Fetches information about a PEP and sends it to the channel.""" + # Refresh the PEP data up to every hour, as e.g. the PEP status might have changed. + embed = await self.get_pep_embed(pep_number) await send_or_reply(ctx, embed) + @app_commands.command(name="pep") + @app_commands.guild_only() + @app_commands.describe(pep_number="The pep number or the autocompleted pep") + async def pep_slash_command(self, interaction: Interaction, pep_number: int) -> bool: + embed = await self.get_pep_embed(pep_number) + await interaction.response.send_message(embed=embed) + + @pep_slash_command.autocomplete("pep_number") + async def pep_slash_command_autocomplete(self, interaction: Interaction, current: str) -> list[app_commands.Choice]: + if self.last_refreshed_peps is None: + await self.refresh_pep_data() + + choices = {pep_id: pep_info["title"] for pep_id, pep_info in self.peps.items()} + + # list[('pep_title', similarity, pep_number)] + result = process.extract(query=current, choices=choices, limit=10) + return [app_commands.Choice(name=f"{pep[2]} - {pep[0]}", value=pep[2]) for pep in result] + + async def setup(bot: Bot) -> None: """Load the PEP cog.""" await bot.add_cog(PythonEnhancementProposals(bot)) From 31b74c40c83768961e336755524856dcfd2f8336 Mon Sep 17 00:00:00 2001 From: Sacul0457Deve <183588943+Sacul0457@users.noreply.github.com.> Date: Sun, 2 Aug 2026 01:40:19 +0800 Subject: [PATCH 2/3] fix code formatting --- bot/exts/info/pep.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index cbd8bb6b82..694bd4b783 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -14,7 +14,6 @@ ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" PEP_API_URL = "https://peps.python.org/api/peps.json" - class PEPInfo(TypedDict): """ Useful subset of the PEP API response. @@ -47,7 +46,9 @@ async def refresh_pep_data(self) -> None: log.trace("Started refreshing PEP data.") async with self.bot.http_session.get(PEP_API_URL) as resp: if resp.status != 200: - log.warning("Fetching PEP data from PEP API failed with code %s", resp.status) + log.warning( + "Fetching PEP data from PEP API failed with code %s", + resp.status) return listing = await resp.json() @@ -73,8 +74,11 @@ def generate_pep_embed(self, pep: PEPInfo) -> Embed: return embed async def get_pep_embed(self, pep_number: int) -> Embed: - if self.last_refreshed_peps is None or ( - (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC) and len(str(pep_number)) < 5 + if ( + self.last_refreshed_peps is None or ( + (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC) + and len(str(pep_number)) < 5 + ) ): await self.refresh_pep_data() From 12208ff584e34ae292635ddeeb37ff864c3a90cc Mon Sep 17 00:00:00 2001 From: Sacul0457Deve <183588943+Sacul0457@users.noreply.github.com.> Date: Sun, 2 Aug 2026 10:04:00 +0800 Subject: [PATCH 3/3] address review: cache autocomplete choices and add datetime check --- bot/exts/info/pep.py | 45 +++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index 694bd4b783..de266146a1 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -1,9 +1,9 @@ from datetime import UTC, datetime, timedelta from typing import TypedDict -from rapidfuzz import process -from discord import Colour, Embed, app_commands, Interaction +from discord import Colour, Embed, Interaction, app_commands from discord.ext.commands import Cog, Context, command +from rapidfuzz import process from bot.bot import Bot from bot.log import get_logger @@ -14,6 +14,7 @@ ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" PEP_API_URL = "https://peps.python.org/api/peps.json" + class PEPInfo(TypedDict): """ Useful subset of the PEP API response. @@ -36,6 +37,7 @@ class PythonEnhancementProposals(Cog): def __init__(self, bot: Bot): self.bot = bot self.peps: dict[int, PEPInfo] = {} + self.pep_autocomplete_choices: dict[int, str] = {} self.last_refreshed_peps: datetime | None = None async def refresh_pep_data(self) -> None: @@ -46,14 +48,14 @@ async def refresh_pep_data(self) -> None: log.trace("Started refreshing PEP data.") async with self.bot.http_session.get(PEP_API_URL) as resp: if resp.status != 200: - log.warning( - "Fetching PEP data from PEP API failed with code %s", - resp.status) + log.warning("Fetching PEP data from PEP API failed with code %s", resp.status) return listing = await resp.json() - for pep_num, pep_info in listing.items(): - self.peps[int(pep_num)] = pep_info + for pep_num_str, pep_info in listing.items(): + pep_num = int(pep_num_str) + self.peps[pep_num] = pep_info + self.pep_autocomplete_choices[pep_num] = pep_info["title"] log.info("Successfully refreshed PEP data.") @@ -73,15 +75,17 @@ def generate_pep_embed(self, pep: PEPInfo) -> Embed: return embed - async def get_pep_embed(self, pep_number: int) -> Embed: - if ( - self.last_refreshed_peps is None or ( - (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC) - and len(str(pep_number)) < 5 - ) - ): + async def refresh_pep_data_if_needed(self, *, pep_number: int | None = None) -> None: + """Refreshes the PEP data only when a certain criteria is met.""" + if self.last_refreshed_peps is None or (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC): + if pep_number is not None and len(str(pep_number)) >= 5: + return await self.refresh_pep_data() + async def get_pep_embed(self, pep_number: int) -> Embed: + """Refreshes the PEP data if needed and generates the PEP embed.""" + await self.refresh_pep_data_if_needed(pep_number=pep_number) + if pep := self.peps.get(pep_number): embed = self.generate_pep_embed(pep) else: @@ -103,19 +107,18 @@ async def pep_command(self, ctx: Context, pep_number: int) -> None: @app_commands.command(name="pep") @app_commands.guild_only() @app_commands.describe(pep_number="The pep number or the autocompleted pep") - async def pep_slash_command(self, interaction: Interaction, pep_number: int) -> bool: + async def pep_slash_command(self, interaction: Interaction, pep_number: int) -> None: + """Fetches information about a PEP and sends it to the channel.""" embed = await self.get_pep_embed(pep_number) await interaction.response.send_message(embed=embed) @pep_slash_command.autocomplete("pep_number") - async def pep_slash_command_autocomplete(self, interaction: Interaction, current: str) -> list[app_commands.Choice]: - if self.last_refreshed_peps is None: - await self.refresh_pep_data() - - choices = {pep_id: pep_info["title"] for pep_id, pep_info in self.peps.items()} + async def pep_slash_command_autocomplete(self, interaction: Interaction, query: str) -> list[app_commands.Choice]: + """Returns a list of PEPs that matches `query`.""" + await self.refresh_pep_data_if_needed() # list[('pep_title', similarity, pep_number)] - result = process.extract(query=current, choices=choices, limit=10) + result = process.extract(query=query, choices=self.pep_autocomplete_choices, limit=10) return [app_commands.Choice(name=f"{pep[2]} - {pep[0]}", value=pep[2]) for pep in result]