Repository: swecc-uw/swecc-course-scraper
Pull request #12: Add initial frequency function
Author: login: kvn147
Created: 2025-05-13T19:30:51Z | Merged: 2025-05-17T21:54:45Z

Hello,

I have added a frequency.py file under the command directory that outputs a specific course codes offerings and frequencies per quarter within the last set amount of years. I then added frequency to cli.py as a command option with args course code and years to check (default 5 years). I checked code quality with the linter, formatting, type checking, and testing.


Diff:
diff --git a/swecc_course_scraper/cli.py b/swecc_course_scraper/cli.py
index b92b02d..86c9937 100644
--- a/swecc_course_scraper/cli.py
+++ b/swecc_course_scraper/cli.py
@@ -1,5 +1,7 @@
 import argparse
 
+from swecc_course_scraper.commands.frequency import DEFAULT_YEARS_CHECK
+from swecc_course_scraper.commands.frequency import command as frequency
 from swecc_course_scraper.commands.login import command as login
 from swecc_course_scraper.commands.schedule import command as schedule
 
@@ -11,6 +13,14 @@ def main(args: argparse.Namespace) -> None:
         elif args.schedule:
             department, quarter, year = args.schedule
             print(schedule(department, quarter, year))
+        elif args.frequency:
+            course_code = args.frequency[0]
+            check_years = (
+                int(args.frequency[1])
+                if len(args.frequency) > 1
+                else DEFAULT_YEARS_CHECK
+            )
+            print(frequency(course_code, check_years))
         else:
             print("No command specified. Use --help to show all commands.")
 
@@ -31,4 +41,18 @@ def main(args: argparse.Namespace) -> None:
         type=str,
         help="Get previous quarters schedules. \n e.g.: --schedule cse [WIN|SPR|SUM|AUT] 2023",
     )
+    parser.add_argument(
+        "--frequency",
+        nargs="+",
+        metavar=("COURSE_CODE", "YEARS_CHECK"),
+        type=str,
+        help=(
+            "Get the frequency a course is offered by quarter within a specified number of"
+            " years from today. \n"
+            "COURSE_CODE: Course code to check \n"
+            f"YEARS_CHECK: Number of years to check from today (Default {DEFAULT_YEARS_CHECK}"
+            " years) \n"
+            "e.g.: --frequency CSE143 5"
+        ),
+    )
     main(parser.parse_args())
diff --git a/swecc_course_scraper/commands/frequency.py b/swecc_course_scraper/commands/frequency.py
new file mode 100644
index 0000000..944b6fb
--- /dev/null
+++ b/swecc_course_scraper/commands/frequency.py
@@ -0,0 +1,70 @@
+from typing import Dict, List
+
+from swecc_course_scraper.commands.schedule import (
+    CURRENT_YEAR,
+    EARLIEST_RECORDED_YEAR,
+    VALID_QUARTERS,
+)
+from swecc_course_scraper.commands.schedule import (
+    command as schedule_command,
+)
+
+# Constant for default years to check
+DEFAULT_YEARS_CHECK = 5
+
+
+def command(course_code: str, check_years: int = DEFAULT_YEARS_CHECK) -> str:
+    """
+    Returns the frequency for a specified course code from multiple quarters in a specified number
+    of years from today.
+
+    Args:
+        str: The course code (e.g., "cse143", "CSE143").
+        int: The number of years to check for course frequency. Default is DEFAULT_YEARS_CHECK.
+
+    Returns:
+        str: Lists course offering frequency.
+    """
+
+    course_department = "".join(filter(str.isalpha, course_code)).lower()
+    course_number = "".join(filter(str.isdigit, course_code))
+    course_code = f"{course_department}{course_number}"
+
+    frequency: Dict[str, int] = {}
+    offerings: List[str] = []
+    total_quarters: int = 0
+
+    earliest_year = max(CURRENT_YEAR - check_years + 1, EARLIEST_RECORDED_YEAR)
+    for year in range(CURRENT_YEAR, earliest_year - 1, -1):
+        for quarter in VALID_QUARTERS:
+            total_quarters += 1
+            try:
+                schedule_data = schedule_command(course_department, quarter, year)
+                if course_code in schedule_data:
+                    frequency[quarter] = frequency.get(quarter, 0) + 1
+                    offerings.append(f"{quarter} {year}")
+            except (FileNotFoundError, ConnectionError):
+                import logging
+
+                logging.exception(
+                    f"Error fetching schedule for {course_department} {quarter} {year}"
+                )
+                continue
+
+    result = [f"Course {course_code.upper()}:"]
+
+    if offerings:
+        result.append(
+            f"Offered {len(offerings)} times for {total_quarters} quarters "
+            f"in the last {check_years} years."
+        )
+        result.append("\nFrequency by quarter:")
+        for quarter in sorted(frequency.keys()):
+            result.append(f"- {quarter}: {frequency[quarter]} times")
+        result.append("\nQuarters offered:")
+        for offering in sorted(offerings):
+            result.append(f"- {offering}")
+    else:
+        result.append("No offerings found for course in the time range.")
+
+    return "\n".join(result)


Reviews:
user: login: Advayp
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T00:46:53Z
user: login: kvn147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:13:41Z
user: login: kvn147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:14:10Z
user: login: kvn147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:14:24Z
user: login: Advayp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T19:17:47Z
user: login: kvn147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T21:45:39Z
user: login: kvn147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T21:45:56Z
user: login: Advayp
body: Looks good!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T21:54:38Z

Review comments:
diff_hunk: @@ -11,6 +12,11 @@ def main(args: argparse.Namespace) -> None:
         elif args.schedule:
             department, quarter, year = args.schedule
             print(schedule(department, quarter, year))
+        elif args.frequency:
+            course_code = args.frequency[0]
+            check_years = 5
path: swecc_course_scraper/cli.py
user: login: Advayp
body: You can remove this line
created_at: 2025-05-15T00:36:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,4 +37,11 @@ def main(args: argparse.Namespace) -> None:
         type=str,
         help="Get previous quarters schedules. \n e.g.: --schedule cse [WIN|SPR|SUM|AUT] 2023",
     )
+    parser.add_argument(
+        "--frequency",
+        nargs="+",
+        metavar=("COURSE_CODE", "YEARS_CHECK"),
+        type=str,
+        help="Get the frequency of a course. \n e.g.: --frequency CSE143 5",
path: swecc_course_scraper/cli.py
user: login: Advayp
body: Include what the `years_check` field means in the help message
created_at: 2025-05-15T00:38:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+from typing import Dict, List
+
+from swecc_course_scraper.commands.schedule import (
+    CURRENT_YEAR,
+    EARLIEST_RECORDED_YEAR,
+    VALID_QUARTERS,
+)
+from swecc_course_scraper.commands.schedule import (
+    command as schedule_command,
+)
+
+
+def command(course_code: str, check_years: int = 5) -> str:
+    """
+    Fetches the frequence for a specified course code from multiple quarters.
+
+    Args:
+        str: The course code (e.g., "cse143", "CSE143").
+        int: The number of years to check for course frequency. Default is 5.
+
+    Returns:
+        str: Lists course offering frequency.
+    """
+
+    course_department = "".join(filter(str.isalpha, course_code)).lower()
+    course_number = "".join(filter(str.isdigit, course_code))
+    course_code = f"{course_department}{course_number}"
+
+    frequency: Dict[str, int] = {}
+    offerings: List[str] = []
+    total_quarters: int = 0
+
+    earliest_year = max(CURRENT_YEAR - check_years + 1, EARLIEST_RECORDED_YEAR)
+    for year in range(CURRENT_YEAR, earliest_year - 1, -1):
+        for quarter in VALID_QUARTERS:
+            total_quarters += 1
+            try:
+                schedule_data = schedule_command(course_department, quarter, year)
+                if course_code in schedule_data:
+                    frequency[quarter] = frequency.get(quarter, 0) + 1
+                    offerings.append(f"{quarter} {year}")
+            except (FileNotFoundError, ConnectionError):
path: swecc_course_scraper/commands/frequency.py
user: login: Advayp
body: Add a log indicating that there was an error maybe?
created_at: 2025-05-15T00:43:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+from typing import Dict, List
+
+from swecc_course_scraper.commands.schedule import (
+    CURRENT_YEAR,
+    EARLIEST_RECORDED_YEAR,
+    VALID_QUARTERS,
+)
+from swecc_course_scraper.commands.schedule import (
+    command as schedule_command,
+)
+
+
+def command(course_code: str, check_years: int = 5) -> str:
+    """
+    Fetches the frequence for a specified course code from multiple quarters.
+
+    Args:
+        str: The course code (e.g., "cse143", "CSE143").
+        int: The number of years to check for course frequency. Default is 5.
+
+    Returns:
+        str: Lists course offering frequency.
+    """
+
+    course_department = "".join(filter(str.isalpha, course_code)).lower()
+    course_number = "".join(filter(str.isdigit, course_code))
+    course_code = f"{course_department}{course_number}"
+
+    frequency: Dict[str, int] = {}
+    offerings: List[str] = []
+    total_quarters: int = 0
+
+    earliest_year = max(CURRENT_YEAR - check_years + 1, EARLIEST_RECORDED_YEAR)
+    for year in range(CURRENT_YEAR, earliest_year - 1, -1):
+        for quarter in VALID_QUARTERS:
+            total_quarters += 1
+            try:
+                schedule_data = schedule_command(course_department, quarter, year)
+                if course_code in schedule_data:
+                    frequency[quarter] = frequency.get(quarter, 0) + 1
+                    offerings.append(f"{quarter} {year}")
+            except (FileNotFoundError, ConnectionError):
path: swecc_course_scraper/commands/frequency.py
user: login: kvn147
body: Added a log exception
created_at: 2025-05-15T02:13:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,4 +37,11 @@ def main(args: argparse.Namespace) -> None:
         type=str,
         help="Get previous quarters schedules. \n e.g.: --schedule cse [WIN|SPR|SUM|AUT] 2023",
     )
+    parser.add_argument(
+        "--frequency",
+        nargs="+",
+        metavar=("COURSE_CODE", "YEARS_CHECK"),
+        type=str,
+        help="Get the frequency of a course. \n e.g.: --frequency CSE143 5",
path: swecc_course_scraper/cli.py
user: login: kvn147
body: Added descriptive help message for years_check.
created_at: 2025-05-15T02:14:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +12,11 @@ def main(args: argparse.Namespace) -> None:
         elif args.schedule:
             department, quarter, year = args.schedule
             print(schedule(department, quarter, year))
+        elif args.frequency:
+            course_code = args.frequency[0]
+            check_years = 5
path: swecc_course_scraper/cli.py
user: login: kvn147
body: Removed redundant line.
created_at: 2025-05-15T02:14:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+from typing import Dict, List
+
+from swecc_course_scraper.commands.schedule import (
+    CURRENT_YEAR,
+    EARLIEST_RECORDED_YEAR,
+    VALID_QUARTERS,
+)
+from swecc_course_scraper.commands.schedule import (
+    command as schedule_command,
+)
+
+
+def command(course_code: str, check_years: int = 5) -> str:
+    """
+    Fetches the frequence for a specified course code from multiple quarters.
path: swecc_course_scraper/commands/frequency.py
user: login: Advayp
body: ```suggestion
    Fetches the frequency for a specified course code from multiple quarters.
```
Small typo
created_at: 2025-05-17T19:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +12,10 @@ def main(args: argparse.Namespace) -> None:
         elif args.schedule:
             department, quarter, year = args.schedule
             print(schedule(department, quarter, year))
+        elif args.frequency:
+            course_code = args.frequency[0]
+            check_years = int(args.frequency[1]) if len(args.frequency) > 1 else 5
path: swecc_course_scraper/cli.py
user: login: Advayp
body: Maybe create a constant to hold the default number of years to check? That way, you only have to change it in one place. Be sure to update your descriptions accordingly.
created_at: 2025-05-17T19:17:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +12,10 @@ def main(args: argparse.Namespace) -> None:
         elif args.schedule:
             department, quarter, year = args.schedule
             print(schedule(department, quarter, year))
+        elif args.frequency:
+            course_code = args.frequency[0]
+            check_years = int(args.frequency[1]) if len(args.frequency) > 1 else 5
path: swecc_course_scraper/cli.py
user: login: kvn147
body: Makes sense. Fixed!
created_at: 2025-05-17T21:45:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+from typing import Dict, List
+
+from swecc_course_scraper.commands.schedule import (
+    CURRENT_YEAR,
+    EARLIEST_RECORDED_YEAR,
+    VALID_QUARTERS,
+)
+from swecc_course_scraper.commands.schedule import (
+    command as schedule_command,
+)
+
+
+def command(course_code: str, check_years: int = 5) -> str:
+    """
+    Fetches the frequence for a specified course code from multiple quarters.
path: swecc_course_scraper/commands/frequency.py
user: login: kvn147
body: Fixed typo
created_at: 2025-05-17T21:45:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: Navneethd8
created_at: 2025-05-15T02:23:09Z
body: Run `ruff check swecc_course_scraper` and `black --check swecc_course_scraper` in your terminal so that the code follows the formatting that we use.

Then commit those changes as well and one of us will review it again.
author_association: NONE