From 638bef6d8771c9f8fd41c4973384572f3edff20b Mon Sep 17 00:00:00 2001 From: meows4h <124093142+meows4h@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:06:27 +0000 Subject: [PATCH 1/3] style: automated lint and format fixes --- helpers/analytics.py | 2 +- helpers/emails.py | 1 + helpers/enrollment.py | 4 +- helpers/grabber.py | 2 +- helpers/test.py | 341 +++++++++++++++++++++++++++++------------- helpers/utilities.py | 2 +- runtest.py | 2 +- 7 files changed, 245 insertions(+), 109 deletions(-) diff --git a/helpers/analytics.py b/helpers/analytics.py index 8b3ba3b..0ee5b90 100644 --- a/helpers/analytics.py +++ b/helpers/analytics.py @@ -474,7 +474,7 @@ def process_analytics(analytics_driver, isbn): for section in process_list: if "user".upper() in section: users = get_int(section.split(" ")[0]) - + # adding a failstate to ensure these values are marked as unknown # and can be easily removed via find & replace if users == -1: diff --git a/helpers/emails.py b/helpers/emails.py index 1a7fe17..8ea817d 100644 --- a/helpers/emails.py +++ b/helpers/emails.py @@ -71,6 +71,7 @@ def update_excel(directory, data, sheet_name): # if instructor already exists, just add the other information # otherwise, make new instructor + # TODO # rewrite this into smaller functions to break it apart a little # could easily be much more functionized, making it easier to work with diff --git a/helpers/enrollment.py b/helpers/enrollment.py index b3ead43..ecea238 100644 --- a/helpers/enrollment.py +++ b/helpers/enrollment.py @@ -26,7 +26,9 @@ def get_enrollment_data(dir, instructor_dict={}): elif section not in enrollment_dict[f"{course}"]: enrollment_dict[f"{course}"][f"{section}"] = [max_enroll, campus] else: - print("I don't think these cases exist (Multiple enrollments for course and section)") + print( + "I don't think these cases exist (Multiple enrollments for course and section)" + ) # instructor things instructor = df["PRIMARY_INSTRUCTOR"][idx] diff --git a/helpers/grabber.py b/helpers/grabber.py index df3895b..03c0e51 100644 --- a/helpers/grabber.py +++ b/helpers/grabber.py @@ -280,7 +280,7 @@ def run_get_email(name): while True: time.sleep(0.7) try: - # TODO + # TODO # implementing webdriverwait to speed up checking times # and make this whole process a little faster and more precise # otherwise, leaving this alone for now as it seems to cause more diff --git a/helpers/test.py b/helpers/test.py index 9e36e44..81fe80e 100644 --- a/helpers/test.py +++ b/helpers/test.py @@ -24,8 +24,10 @@ # this could be done in headless mode w/ more looking over the HTML of the log in # pages, but focusing on the core logic functions for now + class AnalyticsTest(unittest.TestCase): """Testing the analytics.py file.""" + def setUp(self): # error message setup self.null_msg = "get_columns: Null % SQL Section." @@ -43,7 +45,7 @@ def check_columns(self, columns): check = False break return check - + # functionizing the error messages def get_sql_msg(self, value): new_null = self.null_msg.replace("%", value) @@ -56,7 +58,7 @@ def get_sql_msg(self, value): def test_ebook_sql(self): section, columns = alma.get_columns(key="ebook") messages = self.get_sql_msg("Ebook") - + self.assertIsNotNone(section, messages["Null"]) self.assertTrue(section, messages["Empty"]) @@ -66,7 +68,7 @@ def test_ebook_sql(self): def test_access_sql(self): section, columns = alma.get_columns(key="access") messages = self.get_sql_msg("Access") - + self.assertIsNotNone(section, messages["Null"]) self.assertTrue(section, messages["Empty"]) @@ -76,17 +78,17 @@ def test_access_sql(self): def test_physical_sql(self): section, columns = alma.get_columns(key="physical") messages = self.get_sql_msg("Physical") - + self.assertIsNotNone(section, messages["Null"]) self.assertTrue(section, messages["Empty"]) col_check = self.check_columns(columns) self.assertTrue(col_check, messages["Miss"]) - + def test_einventory_sql(self): section, columns = alma.get_columns(key="e-inventory") messages = self.get_sql_msg("E-Inventory") - + self.assertIsNotNone(section, messages["Null"]) self.assertTrue(section, messages["Empty"]) @@ -96,7 +98,7 @@ def test_einventory_sql(self): def test_public_sql(self): section, columns = alma.get_columns(key="public") messages = self.get_sql_msg("Public") - + self.assertIsNotNone(section, messages["Null"]) self.assertTrue(section, messages["Empty"]) @@ -137,7 +139,11 @@ def test_col_len(self): ] num = alma.get_col_len(columns) expected = 14 - self.assertEqual(num, expected, f"get_col_len: Miscounting column count; expected {expected}, got {num}") + self.assertEqual( + num, + expected, + f"get_col_len: Miscounting column count; expected {expected}, got {num}", + ) # selenium functions would go here @@ -148,13 +154,17 @@ def test_setup_sql(self): section, columns = alma.get_columns(key=key_val) sql = alma.setup_sql(section, columns) for command in self.sql_statements: - self.assertTrue(command in sql, f"setup_sql: {key_val} SQL statement missing {command}") + self.assertTrue( + command in sql, + f"setup_sql: {key_val} SQL statement missing {command}", + ) # export and import functions would go here class BookstoreTest(unittest.TestCase): """Testing the bookstore.py file.""" + def setUp(self): self.clean_msg = "str_clean: Value mismatch from expected; % =/= ^" @@ -221,35 +231,57 @@ def test_pull_info(self): comments = "Some Comment" requisition = "2/2/2022 2:22:22 PM" - example_row = [term, subject, code, section, instructor, title, edition, author, isbn, publisher, req, sku, comments, requisition] + example_row = [ + term, + subject, + code, + section, + instructor, + title, + edition, + author, + isbn, + publisher, + req, + sku, + comments, + requisition, + ] row = table[0] - + for idx, cell in enumerate(row): - self.assertEqual(cell, example_row[idx], f"pull_info: {cell} doesn't equal {example_row[idx]}") + self.assertEqual( + cell, + example_row[idx], + f"pull_info: {cell} doesn't equal {example_row[idx]}", + ) class ClassesTest(unittest.TestCase): """Testing the classes.py file.""" + def setUp(self): # individiual preset testing - self.analytics_data = {"Physical MMS Id": { - "Types": ["Physical"], - "Copies": [2], - "Users": [0], - "CDL": [False], - "Link": "https://www.example.com", - "Year": 2024, - "Location": "Valley Library BIB" - }, - "Ebook MMS Id": { - "Types": ["Electronic"], - "Copies": [0], - "Users": [4], - "CDL": [True], - "Link": "https://www.example2.com", - "Platform": "", - "Year": 2020 - }} + self.analytics_data = { + "Physical MMS Id": { + "Types": ["Physical"], + "Copies": [2], + "Users": [0], + "CDL": [False], + "Link": "https://www.example.com", + "Year": 2024, + "Location": "Valley Library BIB", + }, + "Ebook MMS Id": { + "Types": ["Electronic"], + "Copies": [0], + "Users": [4], + "CDL": [True], + "Link": "https://www.example2.com", + "Platform": "", + "Year": 2020, + }, + } self.book_info = { "Title": "Book Title", "Author": "Book Author", @@ -282,7 +314,10 @@ def generate_book_list(self): for num in range(1, book_count + 1): base_course = f"Course 0" base_section = section_templates[get_random(0, len(section_templates) - 1)] - base_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] + base_enroll = [ + get_random(0, 150), + campus_templates[get_random(0, len(campus_templates) - 1)], + ] edition = get_random(0, 20) # sometimes the edition value is left empty if edition == 0: @@ -308,9 +343,9 @@ def generate_book_list(self): "CDL": [False], "Link": "https://www.example3.com", "Year": 2000 + get_random(0, 25), - "Location": "Valley Library BIB" + "Location": "Valley Library BIB", } - + rand = get_random(1, 3) if rand >= 2: rand = get_random(1, 2) @@ -318,7 +353,9 @@ def generate_book_list(self): is_cdl = True else: is_cdl = False - base_platform = platform_templates[get_random(0, len(platform_templates) - 1)] + base_platform = platform_templates[ + get_random(0, len(platform_templates) - 1) + ] base_analytics[f"{num}Electronic"] = { "Types": ["Electronic"], "Copies": [0], @@ -326,7 +363,7 @@ def generate_book_list(self): "CDL": [is_cdl], "Link": "https://www.example4.com", "Platform": base_platform, - "Year": 2000 + get_random(0, 25) + "Year": 2000 + get_random(0, 25), } base_info = { @@ -353,10 +390,17 @@ def generate_book_list(self): # generating sections for the already existing base section section_count = get_random(1, 30) for sec in range(1, section_count + 1): - course_section = section_templates[get_random(0, len(section_templates) - 1)] + course_section = section_templates[ + get_random(0, len(section_templates) - 1) + ] sec_num = course_section + sec - sec_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates) - 1)]] - new_book.add_section(base_course, sec_num, instructor, email, sec_enroll) + sec_enroll = [ + get_random(0, 150), + campus_templates[get_random(0, len(campus_templates) - 1)], + ] + new_book.add_section( + base_course, sec_num, instructor, email, sec_enroll + ) book_sections.append(section_count + 1) @@ -364,21 +408,33 @@ def generate_book_list(self): course_count = get_random(1, 6) for course in range(1, course_count + 1): course_name = f"Course {course}" - course_section = section_templates[get_random(0, len(section_templates) - 1)] - course_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates) - 1)]] - new_book.add_course(course_name, course_section, instructor, email, course_enroll) + course_section = section_templates[ + get_random(0, len(section_templates) - 1) + ] + course_enroll = [ + get_random(0, 150), + campus_templates[get_random(0, len(campus_templates) - 1)], + ] + new_book.add_course( + course_name, course_section, instructor, email, course_enroll + ) section_count = get_random(1, 30) for sec in range(1, section_count + 1): sec_num = course_section + sec - sec_enroll = [get_random(0, 150), campus_templates[get_random(0, len(campus_templates ) - 1)]] - new_book.add_section(course_name, sec_num, instructor, email, sec_enroll) + sec_enroll = [ + get_random(0, 150), + campus_templates[get_random(0, len(campus_templates) - 1)], + ] + new_book.add_section( + course_name, sec_num, instructor, email, sec_enroll + ) book_sections.append(section_count + 1) - + book_sections = sorted(book_sections, reverse=True) self.section_list.append(book_sections) - + # need to add one for the initial set self.course_list.append(course_count + 1) self.book_list.append(new_book) @@ -394,10 +450,20 @@ def test_add_course(self): enroll_data = [99, "D"] self.book.add_course(course_name, section_num, instructor, email, enroll_data) - self.assertEqual(len(self.book.courses), 2, "add_course: Number of courses is incorrect.") - self.assertEqual(len(self.book.sections), 2, "add_course: Number of sections is incorrect.") - self.assertEqual(self.book.sec_size[-1], 1, "add_course: Incorrect sec_size array number.") - self.assertEqual(self.book.total_enroll, 100, "add_course: Total enrollment count is incorrect.") + self.assertEqual( + len(self.book.courses), 2, "add_course: Number of courses is incorrect." + ) + self.assertEqual( + len(self.book.sections), 2, "add_course: Number of sections is incorrect." + ) + self.assertEqual( + self.book.sec_size[-1], 1, "add_course: Incorrect sec_size array number." + ) + self.assertEqual( + self.book.total_enroll, + 100, + "add_course: Total enrollment count is incorrect.", + ) # add_section method def test_add_section(self): @@ -408,31 +474,57 @@ def test_add_section(self): enroll_data = [99, "D"] self.book.add_section(course_name, section_num, instructor, email, enroll_data) - self.assertEqual(len(self.book.courses), 1, "add_section: Number of courses is incorrect.") + self.assertEqual( + len(self.book.courses), 1, "add_section: Number of courses is incorrect." + ) # adding the 0 index as book.sections is a list of lists of sections, with the index # corresponding to the index of the course index - self.assertEqual(len(self.book.sections[0]), 2, "add_section: Number of sections is incorrect.") - self.assertEqual(self.book.sec_size[-1], 2, "add_section: Incorrect sec_size array number.") - self.assertEqual(self.book.total_enroll, 100, "add_section: Total enrollment count is incorrect.") + self.assertEqual( + len(self.book.sections[0]), + 2, + "add_section: Number of sections is incorrect.", + ) + self.assertEqual( + self.book.sec_size[-1], 2, "add_section: Incorrect sec_size array number." + ) + self.assertEqual( + self.book.total_enroll, + 100, + "add_section: Total enrollment count is incorrect.", + ) # add_isbn method def test_add_isbn(self): isbn = 2 self.book.add_isbn(isbn) - self.assertEqual(len(self.book.isbns), 2, "add_isbn: Number of ISBNs is incorrect.") + self.assertEqual( + len(self.book.isbns), 2, "add_isbn: Number of ISBNs is incorrect." + ) self.assertIn(2, self.book.isbns, "add_isbn: Missing ISBN number.") # add_enroll method def test_add_enroll(self): enroll_data = [99, "D"] self.book.add_enroll(enroll_data[1], enroll_data[0]) - self.assertEqual(self.book.total_enroll, 100, "add_enroll: Total enrollment count is incorrect.") - self.assertIn("Ecampus", self.book.campuses, "add_enroll: Missing one of the correct campuses.") + self.assertEqual( + self.book.total_enroll, + 100, + "add_enroll: Total enrollment count is incorrect.", + ) + self.assertIn( + "Ecampus", + self.book.campuses, + "add_enroll: Missing one of the correct campuses.", + ) # add_required method def test_add_required(self): self.book.add_required() - self.assertEqual("Required", self.book.requirement, "add_required: Value did not update properly.") + self.assertEqual( + "Required", + self.book.requirement, + "add_required: Value did not update properly.", + ) # End Book Class testing @@ -445,16 +537,24 @@ def test_max_index(self): base_enroll = [99, "D"] instructor = "Test Name" email = "Test Email" - self.book.add_course(course_name, base_section, instructor, email, base_enroll) + self.book.add_course( + course_name, base_section, instructor, email, base_enroll + ) for sec in range(1, num): sec_num = base_section + sec sec_enroll = [sec + num, "C"] - self.book.add_section(course_name, sec_num, instructor, email, sec_enroll) + self.book.add_section( + course_name, sec_num, instructor, email, sec_enroll + ) max_list = classes.get_max_index(self.book.sec_size) # it finds the first index first, meaning the matching sizes are done by lowest index expected = [4, 3, 2, 0, 1] - self.assertEqual(expected, max_list, "get_max_index: Course maximum section count order does not match expected values.") + self.assertEqual( + expected, + max_list, + "get_max_index: Course maximum section count order does not match expected values.", + ) # get_max_courses testing def test_max_courses(self): @@ -595,42 +695,37 @@ def test_import_data(self): max_courses = classes.process_courses(self.book_list, format_headers, dataframe) classes.process_sections(self.book_list, format_headers, max_courses, dataframe) classes.process_isbns(self.book_list, head_names, main_headers, dataframe) - dataframe = classes.import_data(self.book_list, format_headers, head_names, dataframe) + dataframe = classes.import_data( + self.book_list, format_headers, head_names, dataframe + ) self.assertEqual(len(self.book_list), len(dataframe)) class EmailsTest(unittest.TestCase): """Testing the emails.py file.""" + def setUp(self): book_title = "Book Title" book_author = "Book Author" book_edition = "Book Edition" book_year = "Book Year" book_access = { - "Ebook": { - "Number": 1, - "Link": "ebook cdl link", - "CDL": True - }, - "Print1": { - "Number": 2, - "Link": "print 1 link" - }, - "Print2": { - "Number": 4, - "Link": "print 2 link" - }, - "Audio": { - "Link": "audio link" - } + "Ebook": {"Number": 1, "Link": "ebook cdl link", "CDL": True}, + "Print1": {"Number": 2, "Link": "print 1 link"}, + "Print2": {"Number": 4, "Link": "print 2 link"}, + "Audio": {"Link": "audio link"}, } - self.emails_book = emails.Book(book_title, book_author, book_edition, book_year, book_access) + self.emails_book = emails.Book( + book_title, book_author, book_edition, book_year, book_access + ) inst_name = "Instructor Name" inst_email = "Instructor Email" inst_course = "Course 1" inst_section = "101" - self.emails_instructor = emails.Instructor(inst_name, inst_email, inst_course, inst_section, self.emails_book) + self.emails_instructor = emails.Instructor( + inst_name, inst_email, inst_course, inst_section, self.emails_book + ) # Instructor Class testing @@ -640,18 +735,14 @@ def test_add_book(self): author = "Author 2" edition = "Edition 2" year = "Year 2" - access = {"Ebook": { - "Number": 7, - "Link": "ebook link", - "CDL": False - }} + access = {"Ebook": {"Number": 7, "Link": "ebook link", "CDL": False}} local_book = emails.Book(title, author, edition, year, access) case_courses = ["Course 1", "Course 1", "Course 2"] case_sections = ["101", "102", "101"] for idx, course in enumerate(case_courses): self.emails_instructor.add_book(course, case_sections[idx], local_book) - + self.assertEqual(2, len(self.emails_instructor.data)) self.assertEqual(2, len(self.emails_instructor.data["Course 1"])) self.assertEqual(2, len(self.emails_instructor.data["Course 1"]["101"])) @@ -661,7 +752,6 @@ def test_add_book(self): # could add in more tests beyond just the length checking but the data issues would # be apparent when generating emails as well - # End Instructor Class testing @@ -670,7 +760,7 @@ def test_add_book(self): def test_update_excel(self): bookstore_dir = get_directory(["testing", "test_bookstore.csv"]) output_dir = get_directory(["testing", "test_output.xlsx"]) - + workbook = openpyxl.load_workbook(output_dir) sheet_name = "Test" worksheet = workbook[sheet_name] @@ -731,11 +821,10 @@ def test_update_excel(self): self.assertFalse(reading_val) self.assertFalse(email_val) - # create_email_excel testing def test_create_emails(self): output_dir = get_directory(["testing", "test_output.xlsx"]) - + workbook = openpyxl.load_workbook(output_dir) sheet_name = "Test" worksheet = workbook[sheet_name] @@ -772,7 +861,7 @@ def test_create_emails(self): workbook.save(output_dir) workbook.close() emails.create_email_excel("Test", output_dir) - + final_email_list = sorted(email_ready_list + email_sent_list) email_dir = get_directory(["..", "email_output.xlsx"]) @@ -793,6 +882,7 @@ def test_create_emails(self): class EnrollTest(unittest.TestCase): """Testing the enrollment.py file.""" + def setUp(self): directory = ["testing", "test_enrollment.csv"] self.enroll_dir = get_directory(directory) @@ -816,7 +906,9 @@ def test_get_enrollment_without(self): self.assertTrue(check, "") def test_get_enrollment_with(self): - instr_dict, enroll_dict = enroll.get_enrollment_data(self.enroll_dir, self.instructor_dict) + instr_dict, enroll_dict = enroll.get_enrollment_data( + self.enroll_dir, self.instructor_dict + ) self.assertIn("MTH101", enroll_dict, "") self.assertIn("1", enroll_dict["MTH101"], "") @@ -829,38 +921,43 @@ def test_get_enrollment_with(self): email = self.instructor_dict[name] check = self.check_instructor_dict(self.instructor_dict, name, email) self.assertTrue(check, "") - + class OutlookTest(unittest.TestCase): """Testing the grabber.py file.""" + # every function aside from the importer and exporter deal with selenium webdrivers class GUITest(unittest.TestCase): """Testing the gui.py file.""" + # automated gui testing falls under a similar circumstance to testing selenium # there are ways and methods to do so, but for now, not enough time class HelperGUITest(unittest.TestCase): """Testing the helpergui.py file.""" + # see above GUI notes, automated testing gui not within scope as of right now # additionally, it is easy to manually test and verify results class ModesTest(unittest.TestCase): """Testing the modes.py file.""" + # functions to primarily test here would be the excel sheet updaters - + # enrollment_update testing # analytics_update testing - + # emails_update testing class OutputTest(unittest.TestCase): """Testing the output.py file.""" + # this file hosts a single function for the pure reason of handling # formatting and finishing out the export for an excel file # it should be fine to leave out of testing for now @@ -868,6 +965,7 @@ class OutputTest(unittest.TestCase): class SheetTest(unittest.TestCase): """Testing the sheetmaker.py file.""" + # only testing the version of the function with no gui, no cli interaction # make_excel_sheet testing @@ -882,7 +980,11 @@ def test_get_int(self): expected = [1245, 93478, None, None, None, 65, 109] for idx, value in enumerate(values): result = util.get_int(value) - self.assertEqual(expected[idx], result, f"get_int: result {result} does not match {expected[idx]}.") + self.assertEqual( + expected[idx], + result, + f"get_int: result {result} does not match {expected[idx]}.", + ) # get_clean testing def test_get_clean(self): @@ -890,13 +992,13 @@ def test_get_clean(self): test_names = [ "a book that has examples and wOrdS SOMEthing to do with an example e-reading", "what other EXAMPLE case isn't here", - "'what about words appearing' or something elsewhere" - ] + "'what about words appearing' or something elsewhere", + ] expected = [ "A Book That Has S And To Do With An E-Reading", "What Other Case Isn't Here", - "'What About Appearing' Or Elsewhere" - ] + "'What About Appearing' Or Elsewhere", + ] for idx, name in enumerate(test_names): result = util.get_clean(test_cleaner, name) self.assertEqual(expected[idx], result) @@ -916,7 +1018,11 @@ def test_get_directory(self): test_config = test_config["test"] result = util.get_directory("Test", test_config) expected = get_directory(["testing", "test_config.csv"]) - self.assertEqual(expected, result, f"get_directory: result {result} does not match {expected}.") + self.assertEqual( + expected, + result, + f"get_directory: result {result} does not match {expected}.", + ) # get_filepath testing def test_get_filepath(self): @@ -940,7 +1046,18 @@ def test_get_letter(self): # get_edition_string testing def test_get_edition_string(self): cases = [None, "", "5", 29, 10, "1", 11, 21, 22, 13] - expected = [None, None, "5th", "29th", "10th", "1st", "11th", "21st", "22nd", "13th"] + expected = [ + None, + None, + "5th", + "29th", + "10th", + "1st", + "11th", + "21st", + "22nd", + "13th", + ] for idx, case in enumerate(cases): result = util.get_edition_string(case) self.assertEqual(expected[idx], result) @@ -953,7 +1070,9 @@ def test_get_replace_header(self): format_headers = util.get_format_headers() cases = [["TestNum", "TestSection", "TestEmailInfo"]] for idx, case in enumerate(cases): - result = util.get_replace_header(format_headers[2], case[0], case[1], case[2]) + result = util.get_replace_header( + format_headers[2], case[0], case[1], case[2] + ) for val in case: self.assertIn(val, result) @@ -967,7 +1086,7 @@ def test_get_split_course(self): # skipping get_input and get_enabled as it is an input oriented function - # get_sheet_headers, get_config_headers, and get_string_cleaners + # get_sheet_headers, get_config_headers, and get_string_cleaners # are all also is information grabbing functions # get_row_info testing @@ -982,15 +1101,29 @@ def test_get_campus(self): # filling the rest with non-existent values, but this usually does # not occur in the natural data this script will be managing cases = ["C", "D", "Z", "L", "N", "B", "H", "PDX", "DC", "23432", "CR"] - expected = ["Corvallis", "Ecampus", "International", "LaGrande", "Newport", "Cascades", "Portland", "Portland", "Ecampus", None, None] + expected = [ + "Corvallis", + "Ecampus", + "International", + "LaGrande", + "Newport", + "Cascades", + "Portland", + "Portland", + "Ecampus", + None, + None, + ] for idx, case in enumerate(cases): result = util.get_campus(case) self.assertEqual(expected[idx], result) # skipping set_col_format as it is likely to fluctuate and change + # some helper functions to carry out testing various features + def get_directory(folder_list): curr_dir = os.path.dirname(__file__) for file in folder_list: diff --git a/helpers/utilities.py b/helpers/utilities.py index e7ee531..e6f9c74 100644 --- a/helpers/utilities.py +++ b/helpers/utilities.py @@ -144,7 +144,7 @@ def get_edition_string(num): if edition_num[-2] == "1": edition_num += "th" else: - edition_num += "st" + edition_num += "st" else: edition_num += "st" diff --git a/runtest.py b/runtest.py index 619d676..c340aa7 100644 --- a/runtest.py +++ b/runtest.py @@ -5,5 +5,5 @@ # the helpers folder to allow all other files to properly reference # their dependencies -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From bf1bf7923f9496b3fa10879e5345fe53d85f9d9b Mon Sep 17 00:00:00 2001 From: meows4h Date: Wed, 15 Jul 2026 11:00:00 -0700 Subject: [PATCH 2/3] updating documentation and removing some unused code --- documentation.md | 82 ++++++++++++++++++++++++++++++++++++++++---- helpers/grabber.py | 3 +- helpers/gui.py | 5 --- helpers/helpergui.py | 7 ---- main.py | 7 ++++ 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/documentation.md b/documentation.md index 874bf8b..9f79c9e 100644 --- a/documentation.md +++ b/documentation.md @@ -2,7 +2,18 @@ The goal of this file is to provide a higher level overview of how this script works to help identify issues, improvements, as well as future maintainability. ## At a Glance There are two primary methods that this script runs through: The CLI version and the GUI version. The CLI version is more or less function complete in how I intend for it to function and operate, though it still has some rough edges, especially in a user interaction factor. The GUI version is what will continue to receive updates and more improvements over time. These two versions can be toggled between in the `config.ini` file. ---- + +## Overall Flow +There are four primary functions this whole script sets out to do and maintain as of writing this. These four things are: +1. Creating an Excel sheet from data from a couple of different sources, being the bookstore list for the books, Alma Open Access Analytics, Outlook for emails to help contact professors, and CORE enrollment reports to aid in decision making +2. Creating and formatting emails to compile into an Excel sheet to be fed into PowerAutomate, as well as updating the main Excel sheet with what emails have already been created +3. Updating cached/previously collected information that is stored in CSV files in a modular way to keep time and attention required as low as possible +4. Updating the main Excel sheet with newly scraped data in the case that there is previously missing data that was added to any of the sources + +Points 3 and 4 mainly touch on helping facilitate data collection and updating at later points and as such are not the most intensive or directly important parts of the script. On top of all this, this script has two primary modes with some functions being strictly for one or the other, while others simply have a toggle or require no user input / interaction. These two modes are the CLI and GUI mode, but as of now, the GUI option and implementations are the standard. + +To understand the overall process for creating an Excel sheet, it would be best to look inside `sheetmaker.py` as that is the core of that function in how it interacts with everything else. In short, we start with books from the bookstore that was scraped using the `bookstore.py` functions. This gets saved for later to speed up processing time as `bookstore.csv`. These books are processed, broken down for their components and their ISBN is searched against a couple of different facets in `analytics.py` to pull all the relevant data for them, publication year, access type, internal identification numbers, etc. + ## main.py This file serves as the starting point for the whole script. If the GUI mode is activated, it will start the GUI from `gui.py`, otherwise, it will take an input from the user and pass it over to `sheetmaker.py` if you're making a new sheet or to `modes.py` if you're using the other features. ## analytics.py @@ -109,71 +120,116 @@ Processes the format of the given CORE report enrollment file, returning campus ## grabber.py Deals with setting up and handling pulling data from the Outlook browser to get emails for individual professors. ### process_name +Handles iterating through the names that are shown and put into the Outlook window to grab emails. ### process_suggestion +Helps with grabbing the information from the top of the suggestion box. ### get_email +Grabs the first email from the suggestion box. ### setup_grabber +Starts up and initializes the browser to work with to pull data from Outlook. ### grabber_gui +GUI variation of the implementation across this file. Functions are created in different versions and variants to utilize smaller GUI windows and threading to ensure no issues occur. - set_email_store +Setter function to aid in providing button functionality to the helper GUI. - run_process_suggestion +Smaller form process_suggestion function to pull the raw HTML from the page to find the suggestion box. - run_check_ui +Creates a smaller GUI window to provide instructions on setting up the web window. - run_check_web +Checks if the user has created the email composition window. - run_suggestion_ui +Provides the user with options to select from that were in the suggestion box. - run_get_email +Processes the suggestion box into names and emails. ### email_importer +Imports prior email information into a dictionary. ### email_exporter +Exports email information into a csv file with name email pairs. ## gui.py This hosts all of the primary interactive GUI for the user. ### GUI (Class) - Constructor +This constructor takes in a TKinter root GUI window. Sets up the various facets of the window, including names and sizing. - reset_main -- print_main +Resets the main window to a blank screen. Best used before adding elements. - build_main +Constructs the main screen with all the relevant buttons and functions. - build_emails +Creates the email window to modify the language used in the automated script. - build_headers +Creates the header window to change the names of the headers. - build_advanced +Creates the advanced options window. - build_sheet_outlook +Subscreen for asking the user if they would like to open Outlook prior to creating an Excel sheet. - build_sheet_alma +Subscreen for asking the user if they would like to open Alma prior to creating an Excel sheet. - build_sheet_final +Final function to run to initialize creating an Excel sheet from scratch and resetting the main screen. - build_import_csv +Subscreen to ask if the user wants to import prior csv information for any updating service through the script. - start_analytics_csv +Function to start pulling data from Alma. - start_bookstore_csv +Function to start pulling data from the bookstore. - start_grabber_csv +Function to start pulling data from Outlook for the emails. - start_mode +Handles the main window inputs. - write_cfg +Writes information back to the main configuration file. - write_headers +Writes the information from the header tab back into `headers.ini`. - write_emails +Writes the template information to the `emails.ini` config file. ### start_app +Starts the main GUI application. ## helpergui.py This is a much slimmer and simpler version of the main GUI class, to be something much more modular and additive. ### AddedGUI (Class) - Constructor +Takes a title for the window, otherwise just sets the base components. - reset +Resets the window information. - add_label +Adds a text label to the window. - add_button -### make_window +Adds a button to the window with a passable command. ## modes.py Aids in handling the various functions of the script, modularizing individual pieces into useful functions. ### start_mode +Takes a flag from the CLI main file on which mode to start. ### csv_mode +Submenu to select which csv file to update. ### email_mode +Starts the email creation portion of the script. ### update_mode +Submenu to select which information to update the Excel sheet with. ### emails_csv +Opens Outlook to update the emails csv file. ### analytics_csv +Updates the analytics csv file. ### enrollment_update +Updates missing or empty enrollment information on the Excel sheet. ### analytics_update +Updates empty portions within the analytics portion of the Excel sheet. ### emails_update +Updates the email columns on the Excel sheet. ### get_import +Asks the user if they would like to import prior csv information. ## output.py This script has a single function: outputting an Excel sheet! This is where formatting and such gets handled (i.e. color, column sizing, etc.) ### write_to_sheet +Handles all the formatting and information regarding how colors and columns get processed. ## sheetmaker.py Performs all of the functions related to creating new sheets from scratch. ### make_excel_sheet +Works with other portions of the script to update, finalize, and format an Excel sheet to completion. ## utilties.py This script hosts helpful functions that might be purposeful in multiple places around the various helper and main scripts. On top of this, it also helps to host hard coded data that is not necessary to keep in a configuration file (such as header values!). The organization is to help cut down on lines of code in other places, as well as keep information consistency so updating one variable does update it in all relevant places when needed. @@ -184,22 +240,36 @@ Removes preset phrases and terms from the names of books and authors to reduce t ### get_state Takes strings of "True" or "False" and converts them to bool values. ### get_directory +Takes a string and the config file to pull it from to get a directory. ### get_filepath +Given a direct file path, it will grab the direct path to the file given. ### get_letter +Utilized to convert number of columns into the letter code. ### get_edition_string +Converts a number to have the "st", "nd", "rd", and "th" at the end. ### get_format_headers +Gets the headers and information for the columns with course, section, and instructor infromation. ### get_replace_header +Takes the format headers and automatically replaces the information out from them given the course number, section number, and the added information at the end. ### get_split_course +Splits course codes into individual components of subject and number. ### get_input +Gets an input between a set number range with a text to display to the user. ### get_enabled +Asks the user "y/n" for True or False. ### get_sheet_headers +Returns the headers for the columns that are stored in the config file. ### get_config_headers +Returns the config file read directly from the configuration parser. ### get_string_cleaners +Gets the list of things to parse out of the titles from the bookstore to clean them and consolidate titles and authors. ### get_row_info +Given a row from the dataframe and a key to access it, it error checks against blank information and reads it. ### get_campus +Gets the full campus name given the letter code for the individual campus. ### set_col_format -A ---- +Sets the formatting to a uniform format for the Excel sheet. + ## CSV Storage In order to store all the data in a way that is accessible, quick, and aids in subsequent run times, all pulled data is compiled into `.csv` files, each with their own format. This makes it so we don't have to re-run the bookstore scraper, email grabber, or analytics scraper again every single time we wish to do something. ### analytics.csv @@ -219,7 +289,7 @@ Data is formatted as following: Instructor Name, Email ### enrollment.csv This is just a CORE report exported as a csv for all courses in the desired term. This must be done outside of the script itself. ---- + ## Config Files ### config.ini Primary settings for the script, though some of these are redundant / only used by one half of the script. diff --git a/helpers/grabber.py b/helpers/grabber.py index 03c0e51..198d930 100644 --- a/helpers/grabber.py +++ b/helpers/grabber.py @@ -15,7 +15,8 @@ def process_name(base, flag): - """""" + """Updates the name iteration and flag value to cycle through + different results.""" if flag == 1: temp_list = base.split(" ") base = "" diff --git a/helpers/gui.py b/helpers/gui.py index 1563231..5d34c56 100644 --- a/helpers/gui.py +++ b/helpers/gui.py @@ -54,11 +54,6 @@ def reset_main(self): for child in self.main_tab.winfo_children(): child.destroy() - # used for updating the main screen message - def print_main(self, message): - self.reset_main() - ttk.Label(self.main_tab, text=message).grid(column=0, row=0, sticky=tk.W) - # building the main tab def build_main(self): self.reset_main() diff --git a/helpers/helpergui.py b/helpers/helpergui.py index 964cdf6..12712ea 100644 --- a/helpers/helpergui.py +++ b/helpers/helpergui.py @@ -38,10 +38,3 @@ def add_button(self, text, cmd): column=self.column, row=self.row, sticky=tk.W, padx=5, pady=1 ) self.row += 1 - - -def make_window(title, text): - gui_window = AddedGUI(title=title) - gui_window.reset() - gui_window.add_label(text) - return gui_window diff --git a/main.py b/main.py index 5cb9079..302b772 100644 --- a/main.py +++ b/main.py @@ -12,23 +12,30 @@ # - try / except blocks around data entry code # things like analytics.py could use more error checking to prevent # runtime errors to prevent loss of data and time + # - refactoring / reduction in overhead code times # there is unoptimized solutions and code purely for the case of # making this project functional by the necessary date # example of this is exporting after each round of analytics pulling + # - adding in better modularity + options # there are not many print debug statements, nor direct debugging # in general; not that this is necessary, but it is nice # other options could include allowing ISBN-less bookstore listings # to be recorded, but lots of the options are kinda left as is + # - resolving leftover TODO statements in the various scripts # not all of these are serious needs, requirements, or fixes # as much as they are probably spots i have left notes of # what COULD be done or what hasn't been fully tested + # - adding testcases for selenium functions using hidden github values # could still login using headless functionality and secret key values + # - adding in cost comparison that works without querying the site too many times + # - improving documentation, comments, and overall code structure + # - cross referencing our own ISBN values from the bookstore ISBN values # sometimes we own a different book than what the bookstore has per class # but it is the same book, in which case this could use an automatic cross From bf9bd2f3f86b03e9e4264598905758c0569e6b82 Mon Sep 17 00:00:00 2001 From: meows4h Date: Wed, 15 Jul 2026 13:23:52 -0700 Subject: [PATCH 3/3] further updating documentation --- documentation.md | 123 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 38 deletions(-) diff --git a/documentation.md b/documentation.md index 9f79c9e..092197c 100644 --- a/documentation.md +++ b/documentation.md @@ -10,12 +10,23 @@ There are four primary functions this whole script sets out to do and maintain a 3. Updating cached/previously collected information that is stored in CSV files in a modular way to keep time and attention required as low as possible 4. Updating the main Excel sheet with newly scraped data in the case that there is previously missing data that was added to any of the sources -Points 3 and 4 mainly touch on helping facilitate data collection and updating at later points and as such are not the most intensive or directly important parts of the script. On top of all this, this script has two primary modes with some functions being strictly for one or the other, while others simply have a toggle or require no user input / interaction. These two modes are the CLI and GUI mode, but as of now, the GUI option and implementations are the standard. +Points 3 and 4 mainly touch on helping facilitate data collection and updating at later points and as such are not the most intensive or directly important parts of the script. On top of all this, this script has two primary modes with some functions being strictly for one or the other, while others simply have a toggle or require no user input / interaction. These two modes are the CLI and GUI mode, but as of now, the GUI option and implementations are the standard. On top of this, `utilties.py` are helper functions used across the whole project. -To understand the overall process for creating an Excel sheet, it would be best to look inside `sheetmaker.py` as that is the core of that function in how it interacts with everything else. In short, we start with books from the bookstore that was scraped using the `bookstore.py` functions. This gets saved for later to speed up processing time as `bookstore.csv`. These books are processed, broken down for their components and their ISBN is searched against a couple of different facets in `analytics.py` to pull all the relevant data for them, publication year, access type, internal identification numbers, etc. +### Creating an Excel Sheet +To understand the overall process for creating an Excel sheet, it would be best to look inside `sheetmaker.py` as that is the core of that function in how it interacts with everything else. In short, we start with books from the bookstore that was scraped using the `bookstore.py` functions. This gets saved for later to speed up processing time as `bookstore.csv`. These books are processed via `classes.py`, broken down for their components and their ISBN is searched against a couple of different facets in `analytics.py` to pull all the relevant data for them, publication year, access type, internal identification numbers, etc. These bookstore entries also come with names of instructors alongside class codes, this information is passed onto `grabber.py` to pull the instructor email, which is saved to `emails.csv` once pulled. After all this data is processed, everything from `analytics.csv`, `emails.csv`, and `enrollment.csv` is compiled into the output Excel sheet. The `enrollment.csv` data is read via the `enrollment.py` script. Header names are taken from `headers.ini` + +### Email Formatting +As far as email formatting, it reads directly from the output Excel sheet from the prior step, or at least a sheet that uses matches formatting. It takes in information such as the instructor name, email, course information with section numbers, as well as the basic book information to format links. All of this is conducted within `emails.py`. It then updates the main Excel sheet with which emails have been formatted and presumably sent. Some of the language is taken from the `emails.ini` file. + +### Updating CSV Files +Updating is done via the `modes.py` script to make function calls to the other parts of the project as they need to. These functions all export to their respective csv files as well. + +### Updating the Main Excel Sheet +Updates the main excel sheet using the csv file information if new data gets scraped, also managed through `modes.py`. ## main.py This file serves as the starting point for the whole script. If the GUI mode is activated, it will start the GUI from `gui.py`, otherwise, it will take an input from the user and pass it over to `sheetmaker.py` if you're making a new sheet or to `modes.py` if you're using the other features. + ## analytics.py Anything related to handling data from Alma gets handled through this script. Between setting up the browser, parsing the HTML, and handling inputs for things such as the SQL. ### get_columns @@ -65,17 +76,23 @@ Imports data from the bookstore csv file, mainly used by other parts of the prog ## classes.py Stores the frameworks and methods for interacting with storing and processing book related data. ### Book (Class) -- Contstructor +- **Constructor** + Creating an object requires a dictionary of a couple different values: Related course, section, instructor name and email, enrollment information, the ISBN, book title, author, edition, publisher, requirements state, requisition date, the bookstore comment, and the Alma Analytics data for the given book. The constructor will parse out the information to make it more easily accessible for processing later. -- add_course +- **add_course** + When another entry for a book is found that already exists as an object, the new course information gets added via this function. -- add_section +- **add_section** + Similar to the course entry, but instead adding a section to an existing course within an existing book. -- add_isbn +- **add_isbn** + Adding ISBN values for different variants of the same book. -- add_enroll +- **add_enroll** + Putting in additional enrollment information into the book to track all the campuses and possible enrollment values. -- add_required +- **add_required** + If at any point a bookstore listing has the book as "Required", it sets the book to that status using this function. ### get_max_index Finds the index of the course with the most sections within it in order to aid in reducing column count, as well as ensuring the largest courses are always first (leftmost). @@ -98,12 +115,15 @@ Imports all the data from the master book object list and imports it into the da Handles compiling and creating Excel sheets for PowerAutomate emails. ### Book (Class) This class is a very tiny version of the other class that just takes in some basic information for the purposes of emails. -- Constructor +- **Constructor** + Takes the title, author, edition, year published, and access information. ### Instructor (Class) -- Constructor +- **Constructor** + Takes in the instructor name, email, as well as course, section, and book information per section. -- add_book +- **add_book** + Adds books to a given course section for each professor. ### update_excel Updates the main sheet with marking off what emails have been successfully created. @@ -129,17 +149,23 @@ Grabs the first email from the suggestion box. Starts up and initializes the browser to work with to pull data from Outlook. ### grabber_gui GUI variation of the implementation across this file. Functions are created in different versions and variants to utilize smaller GUI windows and threading to ensure no issues occur. -- set_email_store +- **set_email_store** + Setter function to aid in providing button functionality to the helper GUI. -- run_process_suggestion +- **run_process_suggestion** + Smaller form process_suggestion function to pull the raw HTML from the page to find the suggestion box. -- run_check_ui +- **run_check_ui** + Creates a smaller GUI window to provide instructions on setting up the web window. -- run_check_web +- **run_check_web** + Checks if the user has created the email composition window. -- run_suggestion_ui +- **run_suggestion_ui** + Provides the user with options to select from that were in the suggestion box. -- run_get_email +- **run_get_email** + Processes the suggestion box into names and emails. ### email_importer Imports prior email information into a dictionary. @@ -149,39 +175,56 @@ Exports email information into a csv file with name email pairs. ## gui.py This hosts all of the primary interactive GUI for the user. ### GUI (Class) -- Constructor +- **Constructor** + This constructor takes in a TKinter root GUI window. Sets up the various facets of the window, including names and sizing. -- reset_main +- **reset_main** + Resets the main window to a blank screen. Best used before adding elements. -- build_main +- **build_main** + Constructs the main screen with all the relevant buttons and functions. -- build_emails +- **build_emails** + Creates the email window to modify the language used in the automated script. -- build_headers +- **build_headers** + Creates the header window to change the names of the headers. -- build_advanced +- **build_advanced** + Creates the advanced options window. -- build_sheet_outlook +- **build_sheet_outlook** + Subscreen for asking the user if they would like to open Outlook prior to creating an Excel sheet. -- build_sheet_alma +- **build_sheet_alma** + Subscreen for asking the user if they would like to open Alma prior to creating an Excel sheet. -- build_sheet_final +- **build_sheet_final** + Final function to run to initialize creating an Excel sheet from scratch and resetting the main screen. -- build_import_csv +- **build_import_csv** + Subscreen to ask if the user wants to import prior csv information for any updating service through the script. -- start_analytics_csv +- **start_analytics_csv** + Function to start pulling data from Alma. -- start_bookstore_csv +- **start_bookstore_csv** + Function to start pulling data from the bookstore. -- start_grabber_csv +- **start_grabber_csv** + Function to start pulling data from Outlook for the emails. -- start_mode +- **start_mode** + Handles the main window inputs. -- write_cfg +- **write_cfg** + Writes information back to the main configuration file. -- write_headers +- **write_headers** + Writes the information from the header tab back into `headers.ini`. -- write_emails +- **write_emails** + Writes the template information to the `emails.ini` config file. ### start_app Starts the main GUI application. @@ -189,13 +232,17 @@ Starts the main GUI application. ## helpergui.py This is a much slimmer and simpler version of the main GUI class, to be something much more modular and additive. ### AddedGUI (Class) -- Constructor +- **Constructor** + Takes a title for the window, otherwise just sets the base components. -- reset +- **reset** + Resets the window information. -- add_label +- **add_label** + Adds a text label to the window. -- add_button +- **add_button** + Adds a button to the window with a passable command. ## modes.py