Automates the process of scheduling and managing LeetCode challenges on a Trello board. This repository contains a script that organizes coding challenges by topic, difficulty, and sets due dates for each problem. Designed to streamline the study plan for the "75 LeetCode Questions" approach to mastering coding interviews.
"""
Main script for Trello Card Management.

This script coordinates the primary operations for managing Trello cards, including setting up the Trello board, 
processing LeetCode problems, and managing card retests. The script leverages various utility functions from 
imported modules for specific tasks.

Functions:
    - setup_trello_board: Set up the Trello board by ensuring the necessary lists and labels exist.
    - process_cards: Process all problem cards for the board, populate the "To Do this Week" list, and manage card retests.
    - main: The main function that coordinates the primary operations.

Usage:
    This script is intended to be run as the main entry point. It should be executed after the necessary environment 
    variables and configurations are set.

Author: Alex McGonigle @grannyprogramming
"""

import logging
import json
import os
from datetime import datetime
from modules.config_loader import load_ini_settings, load_config
from modules.board_operations import (
    get_board_id,
    set_board_background_image,
    manage_board_lists,
    create_missing_labels,
)
from modules.card_operations import (
    manage_this_week_list,
    retest_cards,
    process_all_problem_cards,
)

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)


def setup_trello_board(config, settings):
    """
    Set up the Trello board for operations.

    Ensures the board exists, sets its background image, manages its lists, and creates any missing labels.
    """
    board_id = get_board_id(config, settings, settings["BOARD_NAME"])
    if board_id is None:
        logging.error("Failed to retrieve board ID.")
        return None
    set_board_background_image(board_id)
    manage_board_lists(board_id)
    create_missing_labels(board_id)
    return board_id


def process_cards(config, settings, board_id, topics, current_date):
    """
    Process Trello cards for a given board and date.

    Processes all problem cards, populates the "To Do this Week" list, and manages card retests.
    """
    process_all_problem_cards(config, settings, board_id, topics, current_date)
    manage_this_week_list(config, settings, board_id)
    retest_cards(config, settings, settings["BOARD_NAME"], current_date)


def main():
    """
    The main function that coordinates the primary operations.

    Loads configurations, sets up the Trello board, and processes the cards.
    """
    config = load_config()
    settings = load_ini_settings()

    # Load topics from the JSON file
    topics_path = os.environ.get("TOPICS_JSON_PATH")
    with open(topics_path, "r", encoding="utf-8") as file:
        topics = json.load(file)

    current_date = datetime.now()

    # Set up the Trello board
    board_id = setup_trello_board(config, settings)
    if not board_id:
        exit(1)

    process_cards(config, settings, board_id, topics, current_date)

    logging.info("Main script execution completed!")


if __name__ == "__main__":
    main()

src/main.py
GrannyProgramming/trello-leetcoing75-scheduler
"""
Trello Board Management Module.

This module provides utility functions for managing various aspects of Trello boards.
It supports operations such as fetching and setting board background images, managing board lists,
creating and deleting labels on boards, creating boards based on names, and more.

Functions:
    - fetch_image(): Downloads the background image from a specified URL.
    - set_board_background_image(board_id): Sets a custom background image for a specified Trello board.
    - manage_board_lists(board_id): Manages the default and required lists on a Trello board.
    - create_missing_labels(board_id): Creates any missing labels on a specified Trello board based on predefined defaults.
    - fetch_all_list_ids(_config, _settings, board_id): Retrieves all list IDs for a given board.
    - fetch_all_label_ids(_config, _settings, board_id): Retrieves all label IDs for a given board.
    - create_board(_config, _settings, board_name): Creates a new Trello board, deletes default lists and labels, and returns its ID.
    - get_board_id(_config, _settings, board_name): Gets the board ID given a board name or creates it if it doesn't exist.
    - delete_list(_config, _settings, board_id, list_name): Deletes a list on a board.
    - check_list_exists(_config, _settings, board_id, list_name): Checks if a list exists on a board.
    - create_list(_config, _settings, board_id, list_name): Creates a new list on a board.
    - upload_custom_board_background(_config, _settings, member_id, image_filepath): Uploads a custom background image for the board.
    - set_custom_board_background(_config, _settings, board_id, background_id): Sets a custom background for the board.
    - get_member_id(_config, _settings): Retrieves the member ID.
    - get_labels_on_board(_config, _settings, board_id): Fetches all labels on a board.
    - delete_label(_config, _settings, label_id): Deletes a specific label.
    - delete_all_labels(_config, _settings, board_id): Deletes all labels on a board.

Dependencies:
    - os: Provides a way of using operating system-dependent functionality.
    - logging: Used for logging information and error messages.
    - .trello_api: Houses Trello-specific API functions.
    - ._config_loader: Provides functions to load configurations and settings.

Globals:
    - _settings: Global variable storing loaded settings from an INI file.
    - _config: Global variable storing loaded configurations.
    - TRELLO_ENTITY: Dictionary containing constants for different Trello entities.

Author: Alex McGonigle @grannyprogramming
"""

import os
import logging
from .trello_api import download_image, trello_request
from .config_loader import load_ini_settings, load_config

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

_settings = load_ini_settings()
_config = load_config()

# Constants
TRELLO_ENTITY = {"BOARD": "boards", "MEMBER": "members", "LIST": "lists"}


def fetch_image():
    """Fetches the background image from a given URL."""
    return download_image(f"{_config['RAW_URL_BASE']}imgs/background/groot.png")


def set_board_background_image(board_id):
    """Sets the board background image."""
    member_id = get_member_id(_config, _settings)
    if not member_id:
        raise ValueError("Failed to retrieve member ID")

    image_filepath = fetch_image()
    if not image_filepath:
        raise ValueError("Failed to download image")

    background_id = upload_custom_board_background(
        _config, _settings, member_id, image_filepath
    )
    if not background_id:
        raise ValueError("Failed to upload custom board background image")

    if not set_custom_board_background(_config, _settings, board_id, background_id):
        raise ValueError("Failed to set board background image")


def manage_board_lists(board_id):
    """Manages the required lists for a given board."""

    for required_list in _settings["REQUIRED_LISTS"]:
        if not check_list_exists(_config, _settings, board_id, required_list):
            create_list(_config, _settings, board_id, required_list)


def create_missing_labels(board_id):
    """Creates missing labels for a given board."""
    labels = trello_request(_config, _settings, f"{board_id}/labels", entity="boards")
    if labels is None:
        raise ValueError(f"Failed to fetch labels for board with ID: {board_id}")

    label_names = [l.get("name") for l in labels if "name" in l]
    for label, color in _settings["DEFAULT_LABELS_COLORS"].items():
        if label not in label_names:
            trello_request(
                _config,
                _settings,
                "labels",
                "POST",
                entity="boards",
                board_id=board_id,
                name=label,
                color=color,
            )
            logging.info(
                "Created label %s with color %s for board ID: %s",
                label,
                color,
                board_id,
            )


def fetch_all_list_ids(_config, _settings, board_id):
    """Retrieve all list IDs for a given board."""
    response = trello_request(_config, _settings, f"{board_id}/lists")
    if response is None:
        logging.error("Failed to fetch lists for board with ID: %s", board_id)
        return {}
    list_ids = {l["name"]: l["id"] for l in response}
    logging.debug("Fetched list IDs: %s", list_ids)
    return list_ids


def fetch_all_label_ids(_config, _settings, board_id):
    """Retrieve all label IDs for a given board."""
    response = trello_request(
        _config, _settings, "labels", entity="boards", board_id=board_id
    )
    if response is None:
        logging.error("Failed to fetch labels for board with ID: %s", board_id)
        return {}
    return {l["name"]: l["id"] for l in response}


def create_board(_config, _settings, board_name):
    """Create a new Trello board, delete default lists, delete all labels, and return its ID."""
    new_board = trello_request(
        _config, _settings, resource="", method="POST", name=board_name
    )

    # Log the response for debugging
    logging.info("Response from board creation: %s", new_board)

    if new_board and "id" in new_board:
        logging.info("Successfully created board with ID: %s", new_board["id"])

        # Delete default lists for the newly created board
        for default_list in _settings["DEFAULT_LISTS"]:
            if check_list_exists(_config, _settings, new_board["id"], default_list):
                delete_list(_config, _settings, new_board["id"], default_list)

        # Delete all labels for the newly created board
        delete_all_labels(_config, _settings, new_board["id"])

        return new_board["id"]
    else:
        logging.error("Failed to create board with name: %s", board_name)
        return None


def get_board_id(_config, _settings, board_name):
    """Get the board ID given a board name. If the board does not exist or is closed, create it."""
    boards = trello_request(_config, _settings, resource="me/boards", entity="members")

    # Check if an open board with the given name exists
    board_id = next(
        (
            board["id"]
            for board in boards
            if board["name"] == board_name and not board["closed"]
        ),
        None,
    )

    # If board doesn't exist or is closed, create it
    if not board_id:
        board_id = create_board(_config, _settings, board_name)
        logging.info("Created a new board with ID: %s", board_id)
    else:
        logging.info("Using existing board with ID: %s", board_id)

    if not board_id:
        logging.error("Failed to find or create a board with name: %s", board_name)

    return board_id


def delete_list(_config, _settings, board_id, list_name):
    """Delete a list on the board."""
    lists = trello_request(_config, _settings, f"{board_id}/lists")
    list_id = next(lst["id"] for lst in lists if lst["name"] == list_name)
    return trello_request(
        _config,
        _settings,
        f"{list_id}/closed",
        method="PUT",
        entity=TRELLO_ENTITY["LIST"],
        value="true",
    )


def check_list_exists(_config, _settings, board_id, list_name):
    """Check if a list exists on the board."""
    lists = trello_request(_config, _settings, f"{board_id}/lists")
    return any(lst["name"] == list_name for lst in lists)


def create_list(_config, _settings, board_id, list_name):
    """Create a new list on a board."""
    return trello_request(
        _config,
        _settings,
        "",
        method="POST",
        entity=TRELLO_ENTITY["LIST"],
        idBoard=board_id,
        name=list_name,
    )


def upload_custom_board_background(_config, _settings, member_id, image_filepath):
    """Upload a custom background image for the board."""
    endpoint = f"members/{member_id}/customBoardBackgrounds"
    with open(image_filepath, "rb") as file:
        files = {"file": (os.path.basename(image_filepath), file, "image/png")}
        response = trello_request(
            _config, _settings, endpoint, method="POST", entity="", files=files
        )
    return response.get("id") if response else None


def set_custom_board_background(_config, _settings, board_id, background_id):
    """Set a custom background for the board."""
    endpoint = f"{board_id}/prefs/background"
    response = trello_request(
        _config,
        _settings,
        endpoint,
        method="PUT",
        entity=TRELLO_ENTITY["BOARD"],
        value=background_id,
    )
    return response if response else None


def get_member_id(_config, _settings):
    """Retrieve the member ID."""
    response = trello_request(_config, _settings, "me", entity=TRELLO_ENTITY["MEMBER"])
    return response.get("id") if response else None


def get_labels_on_board(_config, _settings, board_id):
    """Fetch all labels on the board."""
    return trello_request(_config, _settings, f"{board_id}/labels")


def delete_label(_config, _settings, label_id):
    """Delete a label."""
    return trello_request(
        _config, _settings, label_id, method="DELETE", entity="labels"
    )


def delete_all_labels(_config, _settings, board_id):
    """Delete all labels on the board."""
    labels = get_labels_on_board(_config, _settings, board_id)
    for label in labels:
        delete_label(_config, _settings, label["id"])

src/modules/board_operations.py
GrannyProgramming/trello-leetcoing75-scheduler
"""
Trello Card Management Module.

This module provides utility functions for managing various aspects of Trello cards.
It supports operations such as checking if a card exists, filtering cards based on labels, 
applying changes to cards, moving cards between lists, processing retrospective and completed cards, 
and more.

Functions:
    - card_exists(_config, _settings, board_id, card_name): Checks if a card exists on a specified board.
    - filter_cards_by_label(cards, _settings): Filters out cards with specific labels defined in _settings.
    - apply_changes_to_cards(_config, _settings, list_ids, cards_to_add): Applies changes to Trello cards, 
      especially for managing the "Do this week" list.
    - get_top_card_from_backlog(_config, _settings, list_ids): Retrieves the top card from the 'Backlog' list.
    - move_card_to_list(_config, _settings, card_id, target_list_id): Moves a card to a specified list.
    - process_retrospective_cards(_config, _settings, board_id, current_date): Processes the retrospective 
      cards based on their labels and due dates.
    - process_completed_cards(_config, _settings, board_id, current_date): Moves completed cards that are due 
      this week to the 'Do this week' list.
    - attach_image_to_card(_config, _settings, card_id, topic): Attaches an image to a specified card.
    - create_topic_label(_config, _settings, board_id, category): Creates a label for a given topic.
    - retest_cards(_config, _settings, board_name, current_date): Processes retest cards for a specified board.
    - manage_this_week_list(__config, __settings, board_id): Ensures the 'To Do this Week' list has the
      required number of cards.
    - get_max_cards_for_week(_settings): Calculates the maximum number of cards required for the week.
    - fetch_cards_from_list(_config, _settings, list_id): Fetches all cards from a given list.
    - process_single_problem_card(_config, _settings, board_id, list_ids, label_ids, topic_label_id, category, problem, due_date, current_date): Creates a Trello card for a single LeetCode problem.
    - process_all_problem_cards(_config, _settings, board_id, topics, current_date): Processes all problem cards for a given board.
    - add_comment_to_card(_config, _settings, card_id, comment_content): Adds a comment to a given card.

Dependencies:
    - logging: Used for logging information and error messages.
    - .trello_api: Houses Trello-specific API functions.
    - .board_operations: Provides functions for board-related operations.
    - .utilities: Contains utility functions related to date parsing and filtering.

Author: Alex McGonigle @grannyprogramming
"""


import logging
from .trello_api import trello_request
from .board_operations import fetch_all_list_ids, get_board_id, fetch_all_label_ids
from .utilities import (
    determine_new_due_date_and_list,
    parse_card_due_date,
    is_due_this_week,
    generate_leetcode_link,
    generate_all_due_dates,
    get_list_name_and_due_date,
    load_comment_from_md_file,
)

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)


def card_exists(_config, _settings, board_id, card_name):
    """Check if a card exists on the board."""
    cards = trello_request(_config, _settings, f"{board_id}/cards")
    return any(card["name"] == card_name for card in cards)


def filter_cards_by_label(cards, _settings):
    """Filter out cards with specific labels."""
    if not cards:
        return []

    # Extracting the label names from the _settings without considering the colors
    exclude_labels = set(
        label_name
        for label_name, _ in _settings["DEFAULT_LABELS_COLORS"].items()
        if label_name in ["Somewhat know", "Do not know", "Know"]
    )

    # Filter out cards that have any of the excluded labels
    return [
        card
        for card in cards
        if not exclude_labels & {label["name"] for label in card["labels"]}
    ]


def apply_changes_to_cards(_config, _settings, list_ids, cards_to_add):
    """Apply the necessary changes to the Trello cards (like pulling cards from backlog)."""
    to_do_this_week_id = list_ids.get("Do this week")
    if not to_do_this_week_id:
        logging.error("To Do this Week ID not found when trying to apply changes.")
        return

    # Fetch the current cards in the "Do this week" list.
    current_cards = fetch_cards_from_list(_config, _settings, to_do_this_week_id)
    if not current_cards:
        current_cards = []

    # Filter out the cards that have the labels "Somewhat know:blue", "Do not know:red", and "Know:green".
    filtered_cards = filter_cards_by_label(current_cards, _settings)

    # Calculate how many more cards are needed in the "Do this week" list to meet the weekly quota.
    cards_needed = get_max_cards_for_week(_settings) - len(filtered_cards)
    cards_to_pull = min(cards_needed, cards_to_add)

    for _ in range(cards_to_pull):
        top_card = get_top_card_from_backlog(_config, _settings, list_ids)
        if top_card:
            move_card_to_list(_config, _settings, top_card["id"], to_do_this_week_id)
        else:
            logging.warning("No more cards to pull from the 'Backlog'.")
            break


def get_top_card_from_backlog(_config, _settings, list_ids):
    """
    Get the top card from the 'Backlog' list.
    """
    backlog_id = list_ids.get("Backlog")
    if not backlog_id:
        logging.error("Backlog ID not found when trying to get the top card.")
        return None
    backlog_cards = fetch_cards_from_list(_config, _settings, backlog_id)
    if not backlog_cards:
        logging.warning("No cards found in the 'Backlog' list.")
        return None
    return backlog_cards[0]


def move_card_to_list(_config, _settings, card_id, target_list_id):
    """
    Move a card to a specified list.
    """
    trello_request(
        _config,
        _settings,
        card_id,  # Just use the card_id as the resource.
        "PUT",
        entity="cards",  # Explicitly mention the entity here.
        idList=target_list_id,
    )
    logging.info("Moved card with ID %s to list with ID %s.", card_id, target_list_id)


def process_retrospective_cards(_config, _settings, board_id, current_date):
    """Process the retrospective cards."""
    list_ids = fetch_all_list_ids(_config, _settings, board_id)
    retrospective_list_name = _settings["REQUIRED_LISTS"][1]  # "Retrospective"
    retrospective_cards = trello_request(
        _config,
        _settings,
        "cards",
        entity="lists",
        list_id=list_ids[retrospective_list_name],
    )

    if retrospective_cards:
        for card in retrospective_cards:
            label_names = [label["name"] for label in card["labels"]]
            new_due_date, list_name = determine_new_due_date_and_list(
                label_names, current_date
            )
            if not list_name:
                continue

            # Update the card's list and due date
            trello_request(
                _config,
                _settings,
                card["id"],
                "PUT",
                entity="cards",
                idList=list_ids[list_name],
                due=new_due_date.isoformat(),
            )


def process_completed_cards(_config, _settings, board_id, current_date):
    """Move completed cards that are due this week to the 'Do this week' list."""
    list_ids = fetch_all_list_ids(_config, _settings, board_id)
    completed_list_name = _settings["REQUIRED_LISTS"][
        0
    ]  # Assuming "Completed" is the first item in REQUIRED_LISTS
    completed_cards = trello_request(
        _config,
        _settings,
        "cards",
        entity="lists",
        list_id=list_ids[completed_list_name],
    )

    for card in completed_cards:
        if is_due_this_week(parse_card_due_date(card["due"]), current_date):
            trello_request(
                _config,
                _settings,
                f"/cards/{card['id']}",
                "PUT",
                idList=list_ids["Do this week"],
            )


def attach_image_to_card(_config, _settings, card_id, topic):
    """Attach an image to a given card."""
    image_url = f"{_config['RAW_URL_BASE']}imgs/cards/{topic}.png"
    response = trello_request(
        _config,
        _settings,
        f"{card_id}/attachments",
        "POST",
        entity="cards",
        url=image_url,
    )
    if not response:
        logging.error("Failed to attach image to card %s", card_id)


def create_topic_label(_config, _settings, board_id, category):
    """Create a label for a given topic."""
    return trello_request(
        _config,
        _settings,
        "/labels",
        "POST",
        entity="boards",
        board_id=board_id,
        name=category,
        color="black",
    )


def retest_cards(_config, _settings, board_name, current_date):
    """Process retest cards for a given board."""
    board_id = get_board_id(_config, _settings, board_name)
    process_retrospective_cards(_config, _settings, board_id, current_date)
    process_completed_cards(_config, _settings, board_id, current_date)
    logging.info("Retest cards processed!")


def manage_this_week_list(__config, __settings, board_id):
    """
    Ensure the 'To Do this Week' list has the required number of cards based on the _settings.
    Cards with specific labels are excluded from this count.
    """
    max_cards = get_max_cards_for_week(__settings)

    # Fetch list IDs and get the ID for "To Do this Week"
    list_ids = fetch_all_list_ids(__config, __settings, board_id)
    to_do_this_week_name = __settings["REQUIRED_LISTS"][2]
    to_do_this_week_id = list_ids.get(to_do_this_week_name)

    # Fetch and filter cards
    cards = fetch_cards_from_list(__config, __settings, to_do_this_week_id)
    filtered_cards = filter_cards_by_label(cards, __settings)

    logging.info("Max cards for the week: %s", max_cards)
    logging.info(
        "Number of filtered cards in 'To Do this Week' list: %s", len(filtered_cards)
    )

    # Calculate the number of cards to pull
    cards_to_pull_count = max_cards - len(filtered_cards)

    logging.info("Need to pull %s cards to meet the weekly quota.", cards_to_pull_count)

    apply_changes_to_cards(__config, __settings, list_ids, cards_to_pull_count)


def get_max_cards_for_week(_settings):
    """Calculate maximum cards for the week."""
    return _settings["PROBLEMS_PER_DAY"] * _settings["WORKDAYS"]


def fetch_cards_from_list(_config, _settings, list_id):
    """Fetch all cards from a given list."""
    logging.debug("Fetching cards for list_id: %s", list_id)
    if not list_id:
        logging.error("list_id is not provided when trying to fetch cards from a list.")
        return None
    return trello_request(_config, _settings, "cards", entity="lists", list_id=list_id)


def add_comment_to_card(_config, _settings, card_id, comment_content):
    """Add a comment to a given card."""
    response = trello_request(
        _config,
        _settings,
        f"{card_id}/actions/comments",
        "POST",
        entity="cards",
        text=comment_content,
    )
    if not response:
        logging.error("Failed to add comment to card %s", card_id)


def process_single_problem_card(
    _config,
    _settings,
    board_id,
    list_ids,
    label_ids,
    topic_label_id,
    category,
    problem,
    due_date,
    current_date,
):
    """
    Create a Trello card for a single LeetCode problem.
    """
    card_name = f"{category}: {problem['title']}"
    if not card_exists(_config, _settings, board_id, card_name):
        difficulty_label_id = label_ids.get(problem["difficulty"])
        if not difficulty_label_id:
            logging.error(
                "Difficulty label not found for problem: %s", problem["title"]
            )
            return
        link = generate_leetcode_link(problem["title"])
        list_name, due_date_for_card = get_list_name_and_due_date(
            due_date, current_date
        )
        card_response = trello_request(
            _config,
            _settings,
            resource="/cards",
            method="POST",
            entity="",
            idList=list_ids.get(list_name),
            name=card_name,
            desc=link,
            idLabels=[difficulty_label_id, topic_label_id],
            due=due_date_for_card.isoformat(),
        )
        if not card_response:
            logging.error("Failed to create card: %s", card_name)
            return
        attach_image_to_card(_config, _settings, card_response["id"], category)
        comment_md_content = load_comment_from_md_file(_settings["COMMENT_MD_PATH"])
        add_comment_to_card(_config, _settings, card_response["id"], comment_md_content)


def process_all_problem_cards(_config, _settings, board_id, topics, current_date):
    """Process all problem cards for a given board."""
    list_ids = fetch_all_list_ids(_config, _settings, board_id)
    label_ids = fetch_all_label_ids(_config, _settings, board_id)
    all_due_dates = generate_all_due_dates(
        topics, current_date, _settings["PROBLEMS_PER_DAY"]
    )
    due_date_index = 0

    for category, problems in topics.items():
        topic_label_response = create_topic_label(
            _config, _settings, board_id, category
        )
        if topic_label_response is None:
            logging.error("Failed to create label for category: %s", category)
            continue
        topic_label_id = topic_label_response["id"]
        for problem in problems:
            process_single_problem_card(
                _config,
                _settings,
                board_id,
                list_ids,
                label_ids,
                topic_label_id,
                category,
                problem,
                all_due_dates[due_date_index],
                current_date,
            )
            due_date_index += 1

src/modules/card_operations.py
GrannyProgramming/trello-leetcoing75-scheduler
"""
Configuration Loading Module.

This module provides utility functions to facilitate loading configuration and settings 
for interacting with the Trello API. Configuration and settings can be loaded from both 
INI files and environment variables.

Functions:
    - load_ini_settings: Loads settings from an INI file and returns them as a dictionary.
    - load_config: Loads API and other related configurations from environment variables.

Dependencies:
    - os: Used to access environment variables.
    - configparser: Used to parse INI configuration files.

Usage:
    Ensure that the required settings are available in ".config/settings.ini" for `load_ini_settings` 
    and the required environment variables are set for `load_config` before invoking these functions.

Note:
    - Keep the ".config/settings.ini" file secure as it might contain sensitive settings.
    - Ensure environment variables such as API_KEY, OAUTH_TOKEN, RAW_URL_BASE, and TOPICS_JSON_PATH 
      are set before using the `load_config` function.

Author: Alex McGonigle @grannyprogramming
"""


import os
import configparser


def load_ini_settings():
    """
    Load application-specific settings from an INI file.
    """
    config = configparser.ConfigParser()
    config.read("config/settings.ini")

    return {
        "BASE_URL": config["TRELLO"]["BASE_URL"],
        "BOARD_NAME": config["TRELLO"]["BOARD_NAME"],
        "DEFAULT_LISTS": config["LISTS"]["DEFAULTS"].split(", "),
        "REQUIRED_LISTS": config["LISTS"]["REQUIRED"].split(", "),
        "START_DAY": int(config["WEEK"]["START_DAY"]),
        "END_DAY": int(config["WEEK"]["END_DAY"]),
        "WORKDAYS": int(config["WEEK"]["WORKDAYS"]),
        "DEFAULT_LABELS_COLORS": dict(
            item.split(":") for item in config["LABELS"]["DEFAULT_COLORS"].split(", ")
        ),
        "PROBLEMS_PER_DAY": int(config["PROBLEMS"]["PROBLEMS_PER_DAY"]),
        "COMMENT_MD_PATH": config["FILES"]["COMMENT_MD_PATH"],
    }


def load_config():
    """
    Load essential configurations from environment variables.
    """
    return {
        "API_KEY": os.environ.get("API_KEY"),
        "OAUTH_TOKEN": os.environ.get("OAUTH_TOKEN"),
        "RAW_URL_BASE": os.environ.get("RAW_URL_BASE"),
        "TOPICS_JSON_PATH": os.environ.get("TOPICS_JSON_PATH"),
    }

src/modules/config_loader.py
GrannyProgramming/trello-leetcoing75-scheduler
"""
Trello API  Module.

This module provides utility functions to interact with the Trello API. It encompasses
a variety of functions for sending requests, constructing URLs, downloading images, and more.

Functions:
    - make_request(url, method, params=None, data=None, timeout=None, files=None): Sends a request to a specified URL and handles exceptions and logging.
    - trello_request(_config, _settings, resource, method="GET", entity="boards", timeout=None, files=None, **kwargs): Sends a request to the Trello API using specified _configurations and _settings.
    - construct_url(base_url, entity, resource, **kwargs): Constructs a URL for the Trello API based on provided parameters.
    - download_image(url, filepath="tmp_image.png"): Downloads an image from a given URL and saves it to a specific path.
    - fetch_cards_from_list(_config, _settings, list_id): Fetches all cards from a given Trello list.

Dependencies:
    - logging: Used for logging information and error messages.
    - requests: Used for making HTTP requests.

Author: Alex McGonigle @grannyprogramming
"""

import logging
import requests


logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)


def make_request(url, method, params=None, data=None, timeout=None, files=None):
    """Send a request and handle exceptions and logging."""
    try:
        with requests.request(
            method, url, params=params, data=data, timeout=timeout, files=files
        ) as response:
            response.raise_for_status()
            return response.json()
    except (requests.RequestException, requests.exceptions.JSONDecodeError) as error:
        logging.error("Request to %s failed. Error: %s", url, error)
        return None


def trello_request(
    _config,
    _settings,
    resource,
    method="GET",
    entity="boards",  # Default to boards if not specified
    timeout=None,
    files=None,
    **kwargs,
):
    """Send a request to Trello API and return the response."""
    # Filter out None values from kwargs
    kwargs = {k: v for k, v in kwargs.items() if v is not None}

    # Construct the URL based on the provided parameters
    url = construct_url(_settings["BASE_URL"], entity, resource, **kwargs)

    query = {"key": _config["API_KEY"], "token": _config["OAUTH_TOKEN"]}
    query.update(kwargs)  # Always add the kwargs to the query parameters

    logging.info("Making a request to endpoint: %s with method: %s", method, url)
    return make_request(url, method, params=query, timeout=timeout, files=files)


def construct_url(base_url, entity, resource, **kwargs):
    """
    Construct the URL by joining base_url, entity, board_id (if provided), list_id (if provided), and resource.
    Ensure that there are no double slashes.
    """
    # Prepare a list to hold all components of the URL.
    url_components = [base_url.rstrip("/")]  # Ensure base_url doesn't end with a slash

    # Add the entity (boards, lists, cards, etc.)
    url_components.append(entity)

    # If card_id is provided, add it
    if "card_id" in kwargs and kwargs["card_id"]:
        url_components.append(kwargs["card_id"])
    else:
        # If board_id is provided, add it
        if "board_id" in kwargs and kwargs["board_id"]:
            url_components.append(kwargs["board_id"])

        # If list_id is provided, add it
        if "list_id" in kwargs and kwargs["list_id"]:
            url_components.append(kwargs["list_id"])

    # Add the resource without any leading slash
    url_components.append(resource.lstrip("/"))

    # Debug logs to identify the issue
    logging.debug("URL Components before cleaning: %s", url_components)

    # Filter out None or empty components and join them with '/'
    cleaned_url = "/".join(filter(None, url_components))

    logging.debug("Constructed URL: %s", cleaned_url)
    return cleaned_url


def download_image(url, filepath="tmp_image.png"):
    """Download an image from a given URL and save it to a specified path."""
    try:
        response = requests.get(url, timeout=10)  # Added timeout of 10 seconds
        if response.status_code == 200:
            with open(filepath, "wb") as file:
                file.write(response.content)
            return filepath
        else:
            logging.error(
                "Failed to download image. HTTP status code: %s", response.status_code
            )
            return None
    except requests.Timeout:
        logging.error("Request to %s timed out.", url)
        return None


def fetch_cards_from_list(_config, _settings, list_id):
    """Fetch all cards from a given list."""
    logging.debug("Fetching cards for list_id: %s", list_id)
    if not list_id:
        logging.error("list_id is not provided when trying to fetch cards from a list.")
        return None
    return trello_request(_config, _settings, "cards", entity="lists", list_id=list_id)

src/modules/trello_api.py
GrannyProgramming/trello-leetcoing75-scheduler
"""
LeetCode Trello Utilities Module.

This module offers utility functions tailored for managing LeetCode problem cards on Trello.
It facilitates operations such as processing single or multiple problem cards, determining due dates, 
parsing and managing dates, and more.

Functions:
    - generate_leetcode_link(title): Generates a direct link to a LeetCode problem based on its title.
    - generate_all_due_dates(topics, current_date, problems_per_day): Generates due dates for every problem, taking into account weekdays.
    - get_list_name_and_due_date(due_date, current_date): Determines the appropriate list name and due date based on the current date.
    - is_due_this_week(due_date, current_date): Checks if a specified due date falls within the current week.
    - get_next_working_day(date): Returns the next working day after a given date, excluding weekends.
    - get_max_cards_for_week(_settings): Calculates the maximum number of cards required for the week.
    - determine_new_due_date_and_list(label_names, current_date): Determines the new due date and list for a card based on its labels.
    - parse_card_due_date(card_due): Parses the 'due' date of a card into a datetime object.
    - load_comment_from_md_file(md_file_path): Loads the content of a markdown file and returns it as a string.

Dependencies:
    - logging: Used for logging information and error messages.
    - os: Provides a way of using operating system-dependent functionality.
    - datetime: Used for date operations and manipulations.

Author: Alex McGonigle @grannyprogramming
"""


import logging
import os
from datetime import timedelta, datetime

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)


def generate_leetcode_link(title):
    """Generate a direct LeetCode problem link based on its title."""
    return f"https://leetcode.com/problems/{title.lower().replace(' ', '-')}/"


def generate_all_due_dates(topics, current_date, problems_per_day):
    """Generate due dates for every problem, considering weekdays."""
    due_dates = []
    total_problems = sum(len(problems) for problems in topics.values())
    day = current_date

    while (
        len(due_dates) < total_problems
    ):  # While loop to ensure all problems get a due date
        if day.weekday() < 5:  # 0-4 denotes Monday to Friday
            for _ in range(
                min(problems_per_day, total_problems - len(due_dates))
            ):  # Ensure we don't overshoot the total problems
                due_dates.append(day)
            day += timedelta(days=1)
        else:
            day += timedelta(days=1)  # Increment the day even if it's a weekend

    return due_dates


def get_list_name_and_due_date(due_date, current_date):
    """Determine the appropriate list name and due date based on the current date."""
    list_name = (
        "Do this week" if is_due_this_week(due_date, current_date) else "Backlog"
    )
    return list_name, due_date


def is_due_this_week(due_date, current_date):
    """Determine if a given due date falls within the current week."""
    start_of_week = current_date - timedelta(days=current_date.weekday())
    end_of_week = start_of_week + timedelta(days=4)
    return start_of_week <= due_date <= end_of_week


def get_next_working_day(date):
    """Get the next working day after a given date, skipping weekends."""
    next_day = date + timedelta(days=1)
    while next_day.weekday() >= 5:  # Skip weekends
        next_day += timedelta(days=1)
    return next_day


def get_max_cards_for_week(_settings):
    """Calculate maximum cards for the week."""
    return _settings["PROBLEMS_PER_DAY"] * _settings["WORKDAYS"]


def determine_new_due_date_and_list(label_names, current_date):
    """Determine the new due date and list based on labels."""
    if "Do not know" in label_names:
        new_due_date = get_next_working_day(current_date)
        return new_due_date, "Do this week"
    elif "Somewhat know" in label_names:
        new_due_date = get_next_working_day(current_date + timedelta(weeks=1))
        list_name = (
            "Do this week"
            if is_due_this_week(new_due_date, current_date)
            else "Backlog"
        )
        return new_due_date, list_name
    elif "Know" in label_names:
        new_due_date = get_next_working_day(current_date + timedelta(weeks=4))
        return new_due_date, "Completed"
    return None, None


def parse_card_due_date(card_due):
    """Parse the 'due' date of a card into a datetime object."""
    return datetime.fromisoformat(card_due.replace("Z", ""))


def load_comment_from_md_file(md_file_path):
    """
    Load the content of the markdown file and return as a string.
    """
    github_workspace = os.environ.get("GITHUB_WORKSPACE", "")
    absolute_path = os.path.join(github_workspace, md_file_path)

    with open(absolute_path, "r", encoding="utf-8") as md_file:
        return md_file.read()

src/modules/utilities.py
GrannyProgramming/trello-leetcoing75-scheduler