From f40e7a95e5a8f843389fcf085ced3410d2f86ada Mon Sep 17 00:00:00 2001 From: Yahya Al-Shamali <42281247+PointlessUsername@users.noreply.github.com> Date: Wed, 28 Jun 2023 19:52:51 -0600 Subject: [PATCH] Improved Docstrings --- mecsimcalc/general_utils.py | 64 +++++++++++--- mecsimcalc/image_utils.py | 150 +++++++++++++++++++++++++------- mecsimcalc/plotting_utils.py | 58 +++++++++--- mecsimcalc/spreadsheet_utils.py | 128 ++++++++++++++++++++------- mecsimcalc/table_utils.py | 65 ++++++++++---- mecsimcalc/text_utils.py | 48 ++++++++-- setup.py | 2 +- 7 files changed, 402 insertions(+), 113 deletions(-) diff --git a/mecsimcalc/general_utils.py b/mecsimcalc/general_utils.py index 7cd0019..7f036ea 100644 --- a/mecsimcalc/general_utils.py +++ b/mecsimcalc/general_utils.py @@ -8,18 +8,43 @@ def input_to_file( input_file: str, metadata: bool = False ) -> Union[io.BytesIO, Tuple[io.BytesIO, str]]: """ - Converts a base64 encoded string into a file object and metadata + >>> input_to_file( + input_file: str, + metadata: bool = False + ) -> Union[io.BytesIO, Tuple[io.BytesIO, str]] - Args: - input_file (str): Base64 encoded string, prefixed with metadata - metadata (bool, optional): Flag to return metadata with the file. (Defaults to False) + Transforms a Base64 encoded string into a file object. Optionally, the file metadata can also be returned. - Raises: - ValueError: If the input string doesn't contain ';base64,' which is required to separate metadata and file data. + # Parameters + input_file : str + A Base64 encoded string prefixed with metadata. + metadata : bool, optional + If set to True, the function also returns the metadata. Default is False. - Returns: - io.BytesIO: If metadata is False, return the decoded file data (The thing you get when you open a file in Python) - (io.BytesIO, str): If metadata is True, returns a tuple containing the decoded file data and its metadata. + # Raises + * `ValueError`: + If the input string does not contain ';base64,' which is required to separate metadata and file data. + + # Returns + * `Union[io.BytesIO, Tuple[io.BytesIO, str]]` : + * `metadata | False` : io.BytesIO - Returns a file object containing the file data. + * `metadata | True` : Tuple[io.BytesIO, str] - Returns a tuple containing the file object and the metadata. + + **Note:** The file object is open and can be used with Python file functions (e.g., file.read()). + + + # Examples + **Without metadata** + >>> input_file = inputs["input_file"] + >>> file = msc.input_to_file(input_file) + + (file is now ready to be used with Python file functions) (e.g., file.read()) + + **With metadata** + >>> input_file = inputs["input_file"] + >>> file, metadata = msc.input_to_file(input_file, metadata=True) + + (metadata holds information about the file, such as the file type) """ if ";base64," not in input_file: raise ValueError("Invalid input: must contain ';base64,'") @@ -33,13 +58,24 @@ def input_to_file( def metadata_to_filetype(metadata: str) -> str: """ - Extracts the file type from the metadata + >>> metadata_to_filetype(metadata: str) -> str + + Extracts the file type from the metadata string. + + # Parameters + metadata : str + A metadata string typically in the form "Data:;base64," - Args: - metadata (str): Metadata string typically in the form "Data:;base64," + # Returns + * `str` : + The extracted file type (e.g., 'csv'). For a Microsoft Excel file, it returns 'xlsx'. - Returns: - str: Extracted file type (e.g. "csv"). For a Microsoft Excel file, it returns "xlsx". + # Example + >>> input_file = inputs["input_file"] + >>> file, metadata = msc.input_to_file(input_file, metadata=True) + >>> file_type = msc.metadata_to_filetype(metadata) + >>> print(file_type) + jpeg """ match = re.search(r"/(.+);base64,", metadata) file_type = match[1] if match else "" diff --git a/mecsimcalc/image_utils.py b/mecsimcalc/image_utils.py index 5f2f733..014c041 100644 --- a/mecsimcalc/image_utils.py +++ b/mecsimcalc/image_utils.py @@ -7,22 +7,44 @@ from mecsimcalc import input_to_file, metadata_to_filetype # Define a dictionary for file type conversions -file_type_mappings = {"jpg": "jpeg", "tif": "tiff", "ico": "x-icon", "svg": "svg+xml", "jpeg": "jpeg", "tiff": "tiff", "x-icon": "x-icon", "svg+xml": "svg+xml"} +file_type_mappings = { + "jpg": "jpeg", + "tif": "tiff", + "ico": "x-icon", + "svg": "svg+xml", + "jpeg": "jpeg", + "tiff": "tiff", + "x-icon": "x-icon", + "svg+xml": "svg+xml", + "png": "png", +} + def file_to_PIL(file: io.BytesIO) -> Image.Image: """ - Transforms a file into a Pillow Image object. + >>> file_to_PIL(file: io.BytesIO) -> Image.Image - Args: - file (io.BytesIO): A file object containing image data (using open with 'rb' mode) + Converts a binary file object into a PIL Image object. - Raises: - ValueError: If the file object doesn't contain image data. + # Parameters + file : io.BytesIO + A binary file object containing image data. - Returns: - PIL.Image.Image: An image object created from the file data. - """ + # Raises + * `ValueError` : + If the file object does not contain valid image data. + + # Returns + * `PIL.Image.Image` : + An image object created from the file data. + + # Examples + >>> input_file = inputs["input_file"] + >>> file = msc.input_to_file(input_file) + >>> image = msc.file_to_PIL(file) + (image is now ready to be used with Pillow functions) + """ try: return Image.open(file) except IOError as e: @@ -33,20 +55,44 @@ def input_to_PIL( input_file: str, get_file_type: bool = False ) -> Union[Image.Image, Tuple[Image.Image, str]]: """ - Decodes a Base64 encoded string into a Pillow image object, and optionally retrieves the file type. + >>> input_to_PIL( + input_file: str, + get_file_type: bool = False + ) -> Union[Image.Image, Tuple[Image.Image, str]] + + Decodes a Base64 encoded string into a PIL Image object. Optionally, the file type can also be returned. + + # Parameters + input_file : str + A Base64 encoded string containing image data. + get_file_type : bool, optional + If set to True, the function also returns the file type. Default is False. - Args: - input_file (str): Base64 encoded string containing image data. - get_file_type (bool, optional): If True, the function also returns the file type. (Defaults to False) + # Returns + * `Union[PIL.Image.Image, Tuple[PIL.Image.Image, str]]` : + * `If get_file_type | False` : PIL.Image.Image - Returns an image object created from the file data. + * `If get_file_type | True` : Tuple[PIL.Image.Image, str] - Returns a tuple containing the image object and the file type. - Returns: - Union[PIL.Image.Image, Tuple[PIL.Image.Image, str]]: If get_file_type is False, a Pillow image object is returned. - If get_file_type is True, a tuple containing the Pillow image object and the file type is returned. + # Examples + **Without file type** + >>> input_file = inputs["input_file"] + >>> image = msc.input_to_PIL(input_file) + + (Image is now ready to be used with Pillow functions) + + + **With file type** + >>> input_file = inputs["input_file"] + >>> image, file_type = msc.input_to_PIL(input_file, get_file_type=True) + >>> print(file_type) + png + + (image is now ready to be used with Pillow functions) """ - # Decode the base64 string into a file-like object and extract metadata + # Decode the base64 string into a binary file object and extract metadata file_data, metadata = input_to_file(input_file, metadata=True) - # Load the file data into a Pillow Image + # Convert the file data into a PIL Image object image = file_to_PIL(file_data) if get_file_type: @@ -67,22 +113,64 @@ def print_image( download_file_type: str = "png", ) -> Union[str, Tuple[str, str]]: """ + >>> print_image( + image: Image.Image, + width: int = 200, + height: int = 200, + original_size: bool = False, + download: bool = False, + download_text: str = "Download Image", + download_file_name: str = "myimg", + download_file_type: str = "png" + ) -> Union[str, Tuple[str, str]] + Transforms a Pillow image into an HTML image, with an optional download link. - Args: - image (PIL.Image.Image): A Pillow image object. - width (int, optional): The width for the displayed image, in pixels. (Defaults to 200) - height (int, optional): The height for the displayed image, in pixels. (Defaults to 200) - original_size (bool, optional): If True, the image will retain its original size. (Defaults to False) - download (bool, optional): If True, a download link will be provided. (Defaults to False) - download_text (str, optional): The text for the download link. (Defaults to "Download Image") - download_file_name (str, optional): The name for the downloaded file. (Defaults to 'myimg') - download_file_type (str, optional): The file type for the downloaded file. (Defaults to "png") - - Returns: - Union[str, Tuple[str, str]]: If download is False, an HTML string containing the image is returned. - If download is True, a tuple containing the HTML string for the image and the download link is returned. + # Parameters + + image : PIL.Image.Image + A Pillow image object. + width : int, optional + The width for the displayed image, in pixels. (Defaults to 200) + height : int, optional + The height for the displayed image, in pixels. (Defaults to 200) + original_size : bool, optional + If True, the image will retain its original size. (Defaults to False) + download : bool, optional + If True, a download link will be provided. (Defaults to False) + download_text : str, optional + The text for the download link. (Defaults to "Download Image") + download_file_name : str, optional + The name for the downloaded file. (Defaults to 'myimg') + download_file_type : str, optional + The file type for the downloaded file. (Defaults to "png") + + # Returns + + * `Union[str, Tuple[str, str]]` : + * `download | False` : str (html image) - Returns an HTML string containing the image + * `download | True` : Tuple[str, str] - Returns a tuple containing the HTML string and the download link + + # Examples + **Without download link, with original size** + >>> input_file = inputs["input_file"] + >>> image = msc.input_to_PIL(input_file) + >>> html_image = msc.print_image(image, original_size=True) + >>> return { + "html_image": html_image + } + + **With download link and original file type** + >>> input_file = inputs["input_file"] + >>> image, file_type = msc.input_to_PIL(input_file, get_file_type=True) + >>> html_image, download_link = msc.print_image(image, download=True, download_file_type = file_type) + >>> return { + "html_image": html_image, + "download_link": download_link + } + """ + # Create a copy for display, preserving the original image display_image = image.copy() diff --git a/mecsimcalc/plotting_utils.py b/mecsimcalc/plotting_utils.py index d728c9c..b442df1 100644 --- a/mecsimcalc/plotting_utils.py +++ b/mecsimcalc/plotting_utils.py @@ -15,19 +15,55 @@ def print_plot( download_file_name: str = "myplot", ) -> Union[str, Tuple[str, str]]: """ + >>> print_plot( + plot_obj: Union[plt.Axes, figure.Figure], + width: int = 500, + dpi: int = 100, + download: bool = False, + download_text: str = "Download Plot", + download_file_name: str = "myplot" + ) -> Union[str, Tuple[str, str]] + Converts a matplotlib plot into an HTML image tag and optionally provides a download link for the image. - Args: - plot_obj (Union[plt.Axes, figure.Figure]): The matplotlib plot to be converted. - width (int, optional): The width of the image in pixels. (Defaults to 500) - dpi (int, optional): The DPI of the image. (Defaults to 100) - download (bool, optional): If set to True, a download link will be provided. (Defaults to False) - download_text (str, optional): The text to be displayed for the download link. (Defaults to "Download Plot") - download_file_name (str, optional): The name of the downloaded file. (Defaults to 'myplot') - - Returns: - Union[str, Tuple[str, str]]: If download is False, returns the HTML image as a string. - If download is True, returns a tuple of the HTML image and the download link as strings. + # Parameters + + plot_obj : Union[plt.Axes, figure.Figure] + The matplotlib plot to be converted. + width : int, optional + The width of the image in pixels. (Defaults to 500) + dpi : int, optional + The DPI of the image. (Defaults to 100) + download : bool, optional + If set to True, a download link will be provided. (Defaults to False) + download_text : str, optional + The text to be displayed for the download link. (Defaults to "Download Plot") + download_file_name : str, optional + The name of the downloaded file. (Defaults to 'myplot') + + # Returns + + Union[str, Tuple[str, str]] : + * `download | False` : str - Returns the HTML image as a string. + * `download | True` : Tuple[str, str] - Returns the HTML image and the download link + + # Examples + **Without Download Link** + >>> fig, ax = plt.subplots() + >>> ax.plot([1, 2, 3], [1, 2, 3]) + >>> plot = msc.print_plot(ax) + >>> return { + "plot": plot + } + + **With Download Link and Custom Download Text** + >>> fig, ax = plt.subplots() + >>> ax.plot([1, 2, 3], [1, 2, 3]) + >>> plot, download_link = msc.print_plot(ax, download=True, download_text="Download My Plot") + >>> return { + "plot": plot, + "download_link": download_link + } """ # Check if plot_obj is a matplotlib Axes object diff --git a/mecsimcalc/spreadsheet_utils.py b/mecsimcalc/spreadsheet_utils.py index 0f99643..752afc1 100644 --- a/mecsimcalc/spreadsheet_utils.py +++ b/mecsimcalc/spreadsheet_utils.py @@ -8,16 +8,32 @@ def file_to_dataframe(file: io.BytesIO) -> pd.DataFrame: """ + >>> file_to_dataframe(file: io.BytesIO) -> pd.DataFrame + Converts a base64 encoded file data into a pandas DataFrame. - Args: - input_file (io.BytesIO): Decoded file data. + # Parameters + + file : io.BytesIO + Decoded file data. + + # Raises + + * `pd.errors.ParserError` : + If the file type is not supported. - Raises: - pd.errors.ParserError: If the file type is not supported. + # Returns + * `pd.DataFrame` : + Returns a DataFrame created from the file data. - Returns: - pd.DataFrame: Returns a DataFrame created from the file data. + # Example + >>> input_file = inputs["input_file"] + >>> file = msc.input_to_file(input_file) + >>> df = msc.file_to_dataframe(file) + >>> print(df) + A B C + 0 1 2 3 + 1 4 5 6 """ # try to read the file as a CSV, if that fails try to read it as an Excel file @@ -36,21 +52,35 @@ def input_to_dataframe( input_file: str, get_file_type: bool = False ) -> Union[pd.DataFrame, Tuple[pd.DataFrame, str]]: """ + >>> input_to_dataframe(input_file: str, get_file_type: bool = False) -> Union[pd.DataFrame, Tuple[pd.DataFrame, str]] + Converts a base64 encoded file data into a pandas DataFrame. - Args: - input_file (str): The base64 encoded file data. - get_file_type (bool, optional): If True, the function also returns the file type (Defaults to False) + # Parameters + + input_file : str + The base64 encoded file data. + get_file_type : bool, optional + If True, the function also returns the file type (Defaults to False). + + # Returns + + * `Union[pd.DataFrame, Tuple[pd.DataFrame, str]]` : + * `get_file_type | False` : pd.DataFrame - Returns a DataFrame created from the file data. + * `get_file_type | True` : Tuple[pd.DataFrame, str] - Returns a tuple containing the DataFrame and the file type. + + # Example + >>> input_file = inputs["input_file"] + >>> df = msc.input_to_dataframe(input_file) + >>> print(df) + A B C + 0 1 2 3 + 1 4 5 6 - Returns: - Union[pd.DataFrame, Tuple[pd.DataFrame, str]]: If get_file_type is False, returns a DataFrame created from the file data. - If get_file_type is True, returns a tuple containing the DataFrame and the file type. """ file_data, metadata = input_to_file(input_file, metadata=True) - # if get_file_type is True return the DataFrame and the file type, - # otherwise just return the DataFrame if get_file_type: return file_to_dataframe(file_data), metadata_to_filetype(metadata) else: @@ -65,29 +95,64 @@ def print_dataframe( download_file_type: str = "csv", ) -> Union[str, Tuple[str, str]]: """ - Creates an HTML table from a pandas DataFrame and optionally provides a download link for the table. + >>> print_dataframe( + df: pd.DataFrame, + download: bool = False, + download_text: str = "Download Table", + download_file_name: str = "mytable", + download_file_type: str = "csv" + ) -> Union[str, Tuple[str, str]] - Args: - df (pd.DataFrame): The DataFrame to be converted. - download (bool, optional): If True, the function also provides a download link. (Defaults to False) - download_text (str, optional): The text to be displayed on the download link. (Defaults to "Download Table") - download_file_name (str, optional): The name of the downloaded file. (Defaults to "myfile") - download_file_type (str, optional): The file type of the download. (Defaults to "csv") + Creates an HTML table from a pandas DataFrame and optionally provides a download link for the table. - Returns: - Union[str, Tuple[str, str]]: If download is False, returns the HTML table as a string. - If download is True, returns a tuple of the HTML table and the HTML download link as strings. + # Parameters + + df : pd.DataFrame + The DataFrame to be converted. + download : bool, optional + If True, the function also provides a download link. (Defaults to False) + download_text : str, optional + The text to be displayed on the download link. (Defaults to "Download Table") + download_file_name : str, optional + The name of the downloaded file. (Defaults to "myfile") + download_file_type : str, optional + The file type of the download file. xlsx or csv (Defaults to "csv") + + # Returns + + * `Union[str, Tuple[str, str]]` : + * `download | False` : str - Returns the HTML table as a string. + * `download | True` : Tuple[str, str] - Returns a tuple containing the HTML table and the HTML download link as strings. + + # Examples + ** Without Download Link ** + >>> input_file = inputs["input_file"] + >>> df = msc.input_to_dataframe(input_file) + >>> table = msc.print_dataframe(df) + >>> return { + "table": table + } + + ** With Download Link for excel file ** + >>> input_file = inputs["input_file"] + >>> df = msc.input_to_dataframe(input_file) + >>> table, download_link = msc.print_dataframe(df, download=True, download_file_type="xlsx") + >>> return { + "table": table, + "download_link": download_link + } """ - - # create HTML table if download is False + # if download is False, return the table as a string if not download: return df.to_html() + # convert the download file type to lowercase download_file_type = download_file_type.lower() - # Create a buffer + # create a buffer to store the file data buf = io.BytesIO() + # if the file type is an alias of excel, convert the DataFrame to an excel file if download_file_type in { "excel", "xlsx", @@ -98,22 +163,23 @@ def print_dataframe( "ods", "odt", }: - # create excel file and download link + # convert the DataFrame to an excel file df.to_excel(buf, index=False) - buf.seek(0) # move the cursor to the beginning of the file + buf.seek(0) encoded_data = ( "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64," + base64.b64encode(buf.read()).decode() ) + + # if the file type does not match an alias of excel, convert the DataFrame to a csv file else: - # create csv file and download link df.to_csv(buf, index=False) buf.seek(0) encoded_data = "data:text/csv;base64," + base64.b64encode(buf.read()).decode() - # Create the download link + # create the download link download_link = f"{download_text}" return df.to_html(), download_link diff --git a/mecsimcalc/table_utils.py b/mecsimcalc/table_utils.py index 362cc70..b5d93bb 100644 --- a/mecsimcalc/table_utils.py +++ b/mecsimcalc/table_utils.py @@ -6,19 +6,37 @@ def table_to_dataframe( column_headers: List[str], rows: List[List[str]] ) -> pd.DataFrame: """ + >>> table_to_dataframe(column_headers: List[str], rows: List[List[str]]) -> pd.DataFrame + Create a DataFrame from given rows and column headers - Args: - column_headers (List[str]): List of column headers. - rows (List[List[str]]): List of rows to be converted into a DataFrame. Each row is a list of strings. + # Parameters + + column_headers : List[str] + List of column headers. + rows : List[List[str]] + List of rows to be converted into a DataFrame. Each row is a list of strings. + + # Raises + + * `ValueError` : + If length of rows is not equal to length of column headers. + + # Returns + + * `pd.DataFrame` : + DataFrame constructed from rows and headers. - Raises: - ValueError: If each row does not have the same length as the column headers. + # Example + >>> column_headers = ["A", "B", "C"] + >>> rows = [["1", "2", "3"], ["4", "5", "6"]] + >>> df = msc.table_to_dataframe(column_headers, rows) + >>> print(df) + A B C + 0 1 2 3 + 1 4 5 6 - Return: - pd.DataFrame: DataFrame constructed from rows and headers. """ - # Ensure that each row has the same length as the column headers for row in rows: if len(row) != len(column_headers): raise ValueError("Each row must have the same length as the column headers") @@ -28,17 +46,30 @@ def table_to_dataframe( def print_table(column_headers: List[str], rows: List[List[str]]) -> str: """ + >>> print_table(column_headers: List[str], rows: List[List[str]]) -> str + Create an HTML table from given rows and column headers. - Args: - column_headers (List[str]): The header for each column. - rows (List[List[str]]): A list of rows (each row is a list of strings). + # Parameters + + column_headers : List[str] + The header for each column. + rows : List[List[str]] + A list of rows (each row is a list of strings). + + # Returns - Return: - str: HTML table. + * `str` : + HTML table. + + # Example + >>> column_headers = ["A", "B", "C"] + >>> rows = [["1", "2", "3"], ["4", "5", "6"]] + >>> table = msc.print_table(column_headers, rows) + >>> return { + "table": table + } """ - # Use DataFrame for table creation - df = table_to_dataframe(column_headers, rows) - # Return the table using pandas to_html - return df.to_html(index=False, border=1, escape=True) + df = table_to_dataframe(column_headers, rows) + return df.to_html(index=True, border=1, escape=True) diff --git a/mecsimcalc/text_utils.py b/mecsimcalc/text_utils.py index 9f045bf..9c93988 100644 --- a/mecsimcalc/text_utils.py +++ b/mecsimcalc/text_utils.py @@ -7,24 +7,56 @@ def string_to_file( download_text: str = "Download File", ) -> str: """ + >>> string_to_file( + text: str, + filename: str = "myfile", + download_text: str = "Download File" + ) -> str + Generates a downloadable text file containing the given text. - Args: - text (str): Text to be downloaded - filename (str, optional): Name of the download file. (Defaults to "myfile") - download_text (str, optional): Text to be displayed as the download link. (Defaults to "Download File") + # Parameters + + text : str + Text to be downloaded + filename : str, optional + Name of the download file. (Defaults to "myfile") + download_text : str, optional + Text to be displayed as the download link. (Defaults to "Download File") + + # Returns + + * `str` : + HTML download link + + # Raises - Raises: - TypeError: If the input text is not a string. + * `TypeError` : + If the input text is not a string. + + # Examples + + **Default** + >>> download_link = msc.string_to_file("Hello World") + >>> return { + "download_link": download_link + } + + **Custom Filename and Download Text** + >>> download_link = msc.string_to_file("Hello World", filename="mytextfile", download_text="Download File Here") + >>> return { + "download_link": download_link" + } - Returns: - str: HTML download link """ # Verify that text is a string if not isinstance(text, str): raise TypeError("text must be a string") + # Remove the file extension from the filename if it exists + filename.removesuffix(".txt") + # Encode the text encoded_text = base64.b64encode(text.encode()).decode() mime_type = "data:text/plain;base64," diff --git a/setup.py b/setup.py index 005d106..ea433cd 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh: long_description = "\n" + fh.read() -VERSION = "0.1.3" +VERSION = "0.1.4" DESCRIPTION = "Useful functions for MecSimCalc.com" # Setting up