From d349563ea38843bd65ab27532e9bcae88a94ea6b Mon Sep 17 00:00:00 2001 From: Clara Cassinat Date: Thu, 15 Feb 2024 02:49:16 +0100 Subject: [PATCH] Improve global code quality & documentation --- api.py | 46 ++++++++++++++++++++ bot.py | 128 +++++++++++++++++++++++++++++++++++++------------------- main.py | 54 ++++++++++++++++++++---- 3 files changed, 177 insertions(+), 51 deletions(-) diff --git a/api.py b/api.py index 6e3a2b5..fc20e19 100644 --- a/api.py +++ b/api.py @@ -5,31 +5,77 @@ class EOLApi: def __init__(self): + """ + Initialize a new EOLApi instance. + """ self.session = requests.Session() self.session.headers.update({"accept": "application/json"}) def __enter__(self): + """ + Support for with statement. + """ return self def __exit__(self, exc_type, exc_val, exc_traceback): + """ + Close the session when the with statement is done. + """ self.close() def close(self): + """ + Close the current session. + """ self.session.close() def _get_results(self, endpoint) -> dict: + """ + Send a GET request to the specified endpoint and return the JSON response. + + Args: + endpoint (str): The API endpoint to send the GET request to. + + Returns: + dict: The JSON response from the API. + """ res = self.session.get(endpoint) res.raise_for_status() return res.json() def get_product_details(self, product: str) -> dict: + """ + Get the details of a specific product. + + Args: + product (str): The name of the product. + + Returns: + dict: The details of the product. + """ endpoint = f"{API_URL}/{product}.json" return self._get_results(endpoint) def get_single_cycle_details(self, product: str, cycle: str) -> dict: + """ + Get the details of a specific cycle of a product. + + Args: + product (str): The name of the product. + cycle (str): The name of the cycle. + + Returns: + dict: The details of the cycle. + """ endpoint = f"{API_URL}/{product}/{cycle}.json" return self._get_results(endpoint) def get_all_products(self): + """ + Get the details of all products. + + Returns: + dict: The details of all products. + """ endpoint = f"{API_URL}/all.json" return self._get_results(endpoint) diff --git a/bot.py b/bot.py index 05aaab4..4a823bd 100644 --- a/bot.py +++ b/bot.py @@ -1,6 +1,7 @@ # Importing necessary modules from disnake.ext import commands from disnake import Embed, ApplicationCommandInteraction +from disnake.ext.commands import InteractionBot # Import InteractionBot # Importing functions and constants from other modules from main import ( @@ -11,36 +12,59 @@ ) from configs import BOT_TOKEN -# Creating a commands.Bot() instance and initializing products from API -bot = commands.Bot() +bot = InteractionBot() products = get_all_products_from_api() # Autocomplete function for product names async def autocomp_products(inter: ApplicationCommandInteraction, input: str): - return [lang for lang in products if input.lower() in lang] + """ + Autocomplete function for product names. + + Args: + inter (ApplicationCommandInteraction): The interaction that triggered the autocomplete. + input (str): The current input of the user. + + Returns: + list: A list of product names that contain the user's input. + """ + # Lowercase the input to make the search case-insensitive + input_lower = input.lower() + return [product for product in products if input_lower in product.lower()] # Event handler for when the bot is ready @bot.event async def on_ready(): + """ + Event handler for when the bot is ready. + Prints a message to the console. + """ print("The bot is ready!") # Slash command to get all available products @bot.slash_command(description="Gets all available products") async def get_products(inter): + """ + Slash command to get all available products. + Sends an embed with a list of all available products. + + Args: + inter (ApplicationCommandInteraction): The interaction that triggered the command. + """ embedList = [] results_per_embed = 35 max_pages = 1 + (len(products) // results_per_embed) # Creating paginated Embed messages for product list - for product in batch(products, results_per_embed): + for product_batch in batch(products, results_per_embed): embed = Embed( title=f"Available products [{len(embedList) + 1 } out of {max_pages}]" ) - embed.add_field(name="", value=",".join(product)) + # Provide a name for the field + embed.add_field(name="Products", value=",".join(product_batch)) embedList.append(embed) await inter.channel.send(embeds=embedList) @@ -59,50 +83,55 @@ async def product_details( default=True, description="Include all minors versions ?" ), ): - embedList = [] + """ + Slash command to get details of all cycles of a given product. + Sends an embed with the details of all cycles of a given product. + + Args: + inter (ApplicationCommandInteraction): The interaction that triggered the command. + product (str): The name of the product. + majors_only (bool): Whether to include all minor versions. + + Returns: + None + """ product_details_cycles = get_product_details_from_api( product=product, majors_only=majors_only ) - # Displaying only the first three cycles if there are more + # Limiting the number of displayed cycles to the first five max_size = 5 - if len(product_details_cycles) >= max_size: - product_details_cycles = product_details_cycles[:max_size] + product_details_cycles = product_details_cycles[:max_size] + # Iterating over each product detail cycle for product_details in product_details_cycles: embed = Embed(title=f"Product {product} - {product_details['cycle']}") - first_separator = False - second_separator = False - # Adding fields for latest build and release date + # Adding fields for latest build and release date if they exist if "latest" in product_details: embed.add_field(name="Latest build", value=product_details["latest"]) - first_separator = True if "latestReleaseDate" in product_details: embed.add_field( name="Latest build release", value=product_details["latestReleaseDate"] ) - first_separator = True - # Adding separator if there are previous fields - if first_separator: + # Adding a separator if there are previous fields + if "latest" in product_details or "latestReleaseDate" in product_details: embed.add_field(name="\u200B", value="\u200B", inline=False) - # Adding fields for end of support and end of life + # Adding fields for end of support and end of life if they exist if "support" in product_details: embed.add_field(name="End of support", value=product_details["support"]) - second_separator = True if "eol" in product_details: embed.add_field(name="End of life", value=product_details["eol"]) - second_separator = True - # Adding separator if there are previous fields - if second_separator: + # Adding a separator if there are previous fields + if "support" in product_details or "eol" in product_details: embed.add_field(name="\u200B", value="\u200B", inline=False) - # Adding fields for LTS status and discontinuation status + # Adding fields for LTS status and discontinuation status if they exist if "lts" in product_details: embed.add_field( name="Is long time support ?", @@ -115,13 +144,12 @@ async def product_details( value="yes" if product_details["discontinued"] else "no", ) - embedList.append(embed) - - await inter.channel.send(embeds=embedList) + # Sending the embed + await inter.channel.send(embed=embed) # Slash command to get details of a single cycle for a given product -@bot.slash_command(description="Gets details of a single cycle") +@bot.slash_command(description="Gets details of a single cycle for a given product.") async def product_cycle( inter, product: str = commands.Param( @@ -129,47 +157,57 @@ async def product_cycle( min_length=3, autocomplete=autocomp_products, ), - cycle: str = commands.Param(max_length=10), + cycle: str = commands.Param(max_length=10, description="Cycle name"), ): + """ + Slash command to get details of a single cycle for a given product. + Sends an embed with the details of a single cycle for a given product. + + Args: + inter (ApplicationCommandInteraction): The interaction that triggered the command. + product (str): The name of the product. + cycle (str): The name of the cycle. + + Returns: + None + """ # Retrieving details of a single cycle from API product_details_cycles = get_single_cycle_details_from_api( product=product, cycle=cycle ) + # Creating an Embed message with the product and cycle as the title embed = Embed(title=f"Product {product} - {cycle}") - first_separator = False - second_separator = False - # Adding fields for latest build and release date + # Adding fields for latest build and release date if they exist if "latest" in product_details_cycles: embed.add_field(name="Latest build", value=product_details_cycles["latest"]) - first_separator = True if "latestReleaseDate" in product_details_cycles: embed.add_field( name="Latest build release", value=product_details_cycles["latestReleaseDate"], ) - first_separator = True - # Adding separator if there are previous fields - if first_separator: + # Adding a separator if there are previous fields + if ( + "latest" in product_details_cycles + or "latestReleaseDate" in product_details_cycles + ): embed.add_field(name="\u200B", value="\u200B", inline=False) - # Adding fields for end of support and end of life + # Adding fields for end of support and end of life if they exist if "support" in product_details_cycles: embed.add_field(name="End of support", value=product_details_cycles["support"]) - second_separator = True if "eol" in product_details_cycles: embed.add_field(name="End of life", value=product_details_cycles["eol"]) - second_separator = True - # Adding separator if there are previous fields - if second_separator: + # Adding a separator if there are previous fields + if "support" in product_details_cycles or "eol" in product_details_cycles: embed.add_field(name="\u200B", value="\u200B", inline=False) - # Adding fields for LTS status and discontinuation status + # Adding fields for LTS status and discontinuation status if they exist if "lts" in product_details_cycles: embed.add_field( name="Is long time support ?", @@ -181,9 +219,15 @@ async def product_cycle( name="Is discontinued ?", value="yes" if product_details_cycles["discontinued"] else "no", ) + # Sending the Embed message - await inter.channel.send(embeds=embed) + await inter.channel.send(embed=embed) # Running the bot with the provided token -bot.run(BOT_TOKEN) +if __name__ == "__main__": + """ + Main execution of the bot. + This block is executed when the script is run directly, not when it is imported as a module. + """ + bot.run(BOT_TOKEN) diff --git a/main.py b/main.py index a47ea7d..37b461a 100644 --- a/main.py +++ b/main.py @@ -1,32 +1,68 @@ from api import EOLApi -from datetime import datetime, timedelta API = EOLApi() def batch(iterable, n=1): + """ + Generator that yields slices of the iterable, each of size n. + + Args: + iterable: The iterable to slice. + n (int): The size of each slice. + + Yields: + slice: Slices of the iterable, each of size n. + """ length = len(iterable) for split in range(0, length, n): yield iterable[split : min(split + n, length)] def get_all_products_from_api(): + """ + Fetches all products from the API. + + Returns: + list: A list of all products. + """ return API.get_all_products() def get_product_details_from_api(product: str, majors_only: bool): + """ + Fetches the details of a product from the API. + + Args: + product (str): The name of the product. + majors_only (bool): Whether to only include major versions. + + Returns: + list: A list of product details. + """ details = API.get_product_details(product=product) if majors_only: - excluded_minors_products = [] - latest_major = 0 - for entry in details: - if entry["cycle"].split(".")[0] != latest_major: - excluded_minors_products.append(entry) - latest_major = entry["cycle"].split(".")[0] - - details = excluded_minors_products + # Sort the details by major version number + details.sort(key=lambda x: int(x["cycle"].split(".")[0])) + + # Group the details by major version number + groups = groupby(details, key=lambda x: x["cycle"].split(".")[0]) + + # Take the first entry from each group + details = [next(group) for _, group in groups] + return details def get_single_cycle_details_from_api(product, cycle): + """ + Fetches the details of a single cycle of a product from the API. + + Args: + product (str): The name of the product. + cycle (str): The name of the cycle. + + Returns: + dict: The details of the cycle. + """ return API.get_single_cycle_details(product=product, cycle=cycle)