feat(Packagers): combined get and post into a file, added condition of no excel file to read and write

This commit is contained in:
darkicewolf50 2024-12-07 19:48:55 -07:00
parent 8e98eeee64
commit a9460dc692
12 changed files with 192 additions and 258 deletions

View File

@ -1,25 +0,0 @@
import yaml
import json
from ReadDB import ReadDatabase
with open("./MockDB/schedule.yaml", "r") as scheduleyml:
ymlschedule = yaml.safe_load(scheduleyml)
def getSchedulePackager():
"""
Formats and allows for the
``REQUIRES``: None
``PROMISES``: ``JSON`` http response ready
``Develop in part by``: Brock T
``Contact``: darkicewolf50@gmail.ocm
"""
return {
"interviewDates": ReadDatabase()
}

View File

@ -1,11 +0,0 @@
Date:
Sept 16:
Meeting Duration: 30 min
Meeting Start Times:
- 10:00 am
- 10:30 am
- 11:00 am
- 11:30 am
- 1:00 pm
- 1:30 pm
- 2:00 pm

View File

@ -1,13 +1,24 @@
import openpyxl import openpyxl
import yaml import yaml
import json
import datetime import datetime
from openpyxl.styles import Font, Border, Side, PatternFill from openpyxl.styles import Font, Border, Side, PatternFill
from openpyxl.formatting.rule import FormulaRule from openpyxl.formatting.rule import FormulaRule
def NoSheet(): def NoSheet():
# sheet with an example properites """
Creates the Template for more data to be added
``REQUIRES``: ``None`` Ensure no other sheets are present, will overwrite them
``PROMISES``: ``XLSX File`` give the template for recuitment for the year
``Develop in part by``: Brock
``Contact``: darkicewolf50@gmail.com
"""
yamlraw = """ yamlraw = """
Recruitment Responses: Recruitment Responses:
- Frist Name (What we should call them): Steve - Frist Name (What we should call them): Steve
@ -39,21 +50,20 @@ Interview TimeTable:
- Category (if not general): Test - Category (if not general): Test
- Interviewer(s) Name(s): Example - Interviewer(s) Name(s): Example
- Status: Dropdown (Options in datahelp) #default is Unknown - Status: Dropdown (Options in datahelp) #default is Unknown
Data Helper: Data Helper And Info:
- Status Dropdown: - Status Dropdown:
- Unknown - Unknown
- Done - Done
- No Show - No Show
- Cancelled/Moved - Cancelled/Moved
- First time Startup: Call getschedule for the year
- How to Add Dropdown: Go into data, click data validation, select list then select the area you want to get values from in the formula spot - How to Add Dropdown: Go into data, click data validation, select list then select the area you want to get values from in the formula spot
""" """
# uses the base above "yaml file" to create the base template
yamlsheet = yaml.safe_load(yamlraw) yamlsheet = yaml.safe_load(yamlraw)
# print(json.dumps(yamlsheet, indent=4))
year_donation = int(str(datetime.datetime.now().year)[2:]) # gets the last two digits of the current year then adds 1 for the current season year_donation = int(str(datetime.datetime.now().year)[2:]) # gets the last two digits of the current year then adds 1 for the current season
file_name = f"OR{year_donation + 1}-L-Interview Data.xlsx" # name based off the 2025 naming system file_name = f"./OR{year_donation + 1}-L-Interview Data.xlsx" # name based off the 2025 naming system
# border style # border style
border = Border( # defualt behaviour is thin border = Border( # defualt behaviour is thin
@ -63,9 +73,6 @@ Data Helper:
bottom=Side(style='thin') bottom=Side(style='thin')
) )
# for conditional formatting
red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")
# create workbook in memory # create workbook in memory
work_book = openpyxl.Workbook() work_book = openpyxl.Workbook()
@ -73,45 +80,50 @@ Data Helper:
default_sheet = work_book.active default_sheet = work_book.active
work_book.remove(default_sheet) work_book.remove(default_sheet)
# decomposes the yaml file at the top and convertss it into a standard template
# does one sheet at a time
for sheet_name, title_list in yamlsheet.items(): for sheet_name, title_list in yamlsheet.items():
# add standard sheets # add 1 standard sheet, by the outermost name
sheet = work_book.create_sheet(sheet_name) sheet = work_book.create_sheet(sheet_name)
# gets header titles for each sheet, from the inner list
titles = [list(title.keys())[0] for title in title_list] titles = [list(title.keys())[0] for title in title_list]
# makes the header titles to bold, have a border and have text
for col_num, title in enumerate(titles, start=1): for col_num, title in enumerate(titles, start=1):
cell = sheet.cell(row=1, column=col_num) cell = sheet.cell(row=1, column=col_num)
cell.value = title cell.value = title
cell.font = Font(bold=True) cell.font = Font(bold=True)
cell.border = border cell.border = border
# example data to show on what it will look like or to copy formatting down
example_data = [list(data.values())[0] for data in title_list] example_data = [list(data.values())[0] for data in title_list]
for col_num, data in enumerate(example_data, start=1): for col_num, data in enumerate(example_data, start=1):
# for special case Data Helper where there a list in a dictionary
if isinstance(data, list): if isinstance(data, list):
row_num = 2 row_num = 2
for item in data: for item in data:
cell = sheet.cell(row=row_num, column=col_num) cell = sheet.cell(row=row_num, column=col_num)
cell.value = item cell.value = item
row_num += 1 row_num += 1
# adds data to the cells
else: else:
cell = sheet.cell(row=2, column=col_num) cell = sheet.cell(row=2, column=col_num)
cell.value = data # changes the Dropdown data in status to unknown instead of the other option, only there for prep for a dropdown
if data == "Dropdown (Options in datahelp)": if data == "Dropdown (Options in datahelp)":
cell.value = "Unknown" cell.value = "Unknown"
else:
cell.value = data
if sheet.title == "Recruitment Responses": if sheet.title == "Recruitment Responses":
sheet.conditional_formatting.add("A2:I2", FormulaRule(formula=['=$I2="No"'], fill=PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid"))) sheet.conditional_formatting.add("A2:I2", FormulaRule(formula=['=$I2="No"'], fill=PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")))
# save to storage # save to storage
work_book.save(file_name) work_book.save(file_name)
print(f"Created {file_name} for {year_donation}")
if __name__ == "__main__": if __name__ == "__main__":
NoSheet() NoSheet()

Binary file not shown.

106
ReadDB.py
View File

@ -3,74 +3,84 @@ import json
from filelock import FileLock, Timeout from filelock import FileLock, Timeout
import time import time
# Define the path to the Excel file and the lock file from NoSheet import NoSheet
excel_file_path = "./OR-L-Interview Data.xlsx" import datetime
lock_file_path = "./OR-L-Interview Data.xlsx.lock" import os
""" """
TODO change to dynamic file name TODO update to possibly not use pandas and update to use the new template
year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season TODO update name of function to be more clear
file_name = f"OR{year_donation}-L-Interview Data.xlsx" # name based off the 2025 naming system
""" """
def ReadDatabase(): def ReadDatabase():
""" """
Reads the database for which slots are available Reads the database for which slots are available
``REQUIRES``: None ``REQUIRES``: ``None``
``PROMISES``: JSON (Interview Available Slots) ``PROMISES``: ``JSON`` Interview Available Slots
``Developed in part by``: Ahmad ``Developed in part by``: Ahmad, Brock
``Contact``: ahmad.ahmad1@ucalgary.ca ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com
""" """
# Retry parameters
max_retries = 60 # Maximum number of retries if the file is locked
retry_interval = 0.5 # Wait time (in seconds) between retries
retries = 0 year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season
while retries < max_retries: # name based off the 2025 naming system
try: # Define the path to the Excel file and the lock file
# Attempt to acquire a shared read (non-blocking) access excel_file_path = f"OR{year_donation}-L-Interview Data.xlsx"
with FileLock(lock_file_path, timeout=0): # Non-blocking, checks if the lock exists lock_file_path = f"OR{year_donation}-L-Interview Data.xlsx.lock"
# Load the Excel file into a pandas DataFrame
df = pd.read_excel(excel_file_path)
# Initialize the dictionary to store the structured data if not (os.path.isfile(excel_file_path) or os.path.isfile(lock_file_path)):
interview_data = {} NoSheet()
else:
# Retry parameters
max_retries = 60 # Maximum number of retries if the file is locked
retry_interval = 0.5 # Wait time (in seconds) between retries
# Group the DataFrame by Date, Start Time, and Slot for organization retries = 0
for _, row in df.iterrows(): while retries < max_retries:
date = str(row['Date']) try:
start_time = str(row['Start Time']) # Attempt to acquire a shared read (non-blocking) access
slot = int(row['Slot']) if not pd.isna(row['Slot']) else 0 with FileLock(lock_file_path, timeout=0): # Non-blocking, checks if the lock exists
# Load the Excel file into a pandas DataFrame
df = pd.read_excel(excel_file_path)
# Returns the number of interviewees in the slot; returns 0 if empty # Initialize the dictionary to store the structured data
interviewee_amount = len(str(row['Interviewee Name']).split()) if str(row['Interviewee Name']) != "nan" else 0 interview_data = {}
# Check if the slot is available for an interviewee to attend # Group the DataFrame by Date, Start Time, and Slot for organization
available_slots = interviewee_amount != slot for _, row in df.iterrows():
if available_slots: date = str(row['Date'])
# Initialize nested structure if not present start_time = str(row['Start Time'])
if date not in interview_data: slot = int(row['Slot']) if not pd.isna(row['Slot']) else 0
interview_data[date] = {}
# Add the start time and duration if not present
if start_time not in interview_data[date]:
interview_data[date][start_time] = {
'Meeting Duration': row['Meeting Duration'],
}
return interview_data # Successfully read the database
except Timeout: # Returns the number of interviewees in the slot; returns 0 if empty
# File is locked; wait and retry interviewee_amount = len(str(row['Interviewee Name']).split()) if str(row['Interviewee Name']) != "nan" else 0
retries += 1
print(f"File is locked, retrying ({retries}/{max_retries})...")
time.sleep(retry_interval)
# If max retries are exceeded, raise an error # Check if the slot is available for an interviewee to attend
raise RuntimeError("Unable to access the database after multiple attempts due to a file lock.") available_slots = interviewee_amount != slot
if available_slots:
# Initialize nested structure if not present
if date not in interview_data:
interview_data[date] = {}
# Add the start time and duration if not present
if start_time not in interview_data[date]:
interview_data[date][start_time] = {
'Meeting Duration': row['Meeting Duration'],
}
return interview_data # Successfully read the database
except Timeout:
# File is locked; wait and retry
retries += 1
print(f"File is locked, retrying ({retries}/{max_retries})...")
time.sleep(retry_interval)
# If max retries are exceeded, raise an error
raise RuntimeError("Unable to access the database after multiple attempts due to a file lock.")
# Example usage of the ReadDatabase function # Example usage of the ReadDatabase function
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,105 +4,126 @@ from openpyxl import load_workbook
from send_email import send_email from send_email import send_email
from filelock import FileLock from filelock import FileLock
# Define the path to the Excel file and the lock file from NoSheet import NoSheet
file_path = "./interview_database.xlsx" import datetime
lock_path = "./interview_database.xlsx.lock" # Lock file for synchronization import os
""" """
TODO chnage to dynamic file name TODO update to possibly not use pandas and update to use the new template
year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season TODO update name of functions to be more clear
file_name = f"OR{year_donation}-L-Interview Data.xlsx" # name based off the 2025 naming system
""" """
def ReadDatabase(): def ReadDatabase():
""" """
Reads the Database to retrieve available interview slots. Reads the Database to retrieve available interview slots
``REQUIRES``: None ``REQUIRES``: None
``PROMISES``: JSON (Available interview slots) ``PROMISES``: JSON (Available interview slots)
``Developed by``: Ahmad ``Developed by``: Ahmad, Brock
``Contact``: ahmad.ahmad1@ucalgary.ca ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com
""" """
# Use a file-based lock for thread-safe and process-safe access
with FileLock(lock_path):
# Load the Excel file into a pandas DataFrame with specific columns
df = pd.read_excel(file_path, usecols=['Date', 'Start Time', 'Slot', 'Interviewee Name', 'Interviewee Email', 'Meeting Duration'])
# Initialize the dictionary to store structured data for available slots year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season
interview_data = {} # name based off the 2025 naming system
file_path = f"OR{year_donation}-L-Interview Data.xlsx"
lock_path = f"OR{year_donation}-L-Interview Data.xlsx.lock"
# Process each row in the DataFrame to structure data by date and time # checks for if the file exisits for the year otherwise it will create one
for _, row in df.iterrows(): if not (os.path.isfile(file_path) or os.path.isfile(lock_path)):
# Convert Date and Start Time to string format for easier comparison NoSheet()
date = str(row['Date']).split(" ")[0] # Format date to YYYY-MM-DD else:
start_time = str(row['Start Time']) # Use a file-based lock for thread-safe and process-safe access
with FileLock(lock_path):
# Load the Excel file into a pandas DataFrame with specific columns
df = pd.read_excel(file_path, usecols=['Date', 'Start Time', 'Slot', 'Interviewee Name', 'Interviewee Email', 'Meeting Duration'])
# Calculate the slot capacity and current number of interviewees # Initialize the dictionary to store structured data for available slots
slot_capacity = int(row['Slot']) if not pd.isna(row['Slot']) else 0 interview_data = {}
interviewee_names = [name.strip() for name in str(row['Interviewee Name']).split(',') if name.strip()]
interviewee_count = len(interviewee_names) if interviewee_names != ["nan"] else 0
# Check if there are available slots for more interviewees # Process each row in the DataFrame to structure data by date and time
if interviewee_count < slot_capacity: for _, row in df.iterrows():
# Organize data by date and time, keeping track of available slots and meeting duration # Convert Date and Start Time to string format for easier comparison
if date not in interview_data: date = str(row['Date']).split(" ")[0] # Format date to YYYY-MM-DD
interview_data[date] = {} start_time = str(row['Start Time'])
interview_data[date][start_time] = {
'Meeting Duration': row['Meeting Duration'],
'Available Slots': slot_capacity - interviewee_count
}
return interview_data # Calculate the slot capacity and current number of interviewees
slot_capacity = int(row['Slot']) if not pd.isna(row['Slot']) else 0
interviewee_names = [name.strip() for name in str(row['Interviewee Name']).split(',') if name.strip()]
interviewee_count = len(interviewee_names) if interviewee_names != ["nan"] else 0
# Check if there are available slots for more interviewees
if interviewee_count < slot_capacity:
# Organize data by date and time, keeping track of available slots and meeting duration
if date not in interview_data:
interview_data[date] = {}
interview_data[date][start_time] = {
'Meeting Duration': row['Meeting Duration'],
'Available Slots': slot_capacity - interviewee_count
}
return interview_data
def AppendAppointment(date, start_time, interviewee_name, interviewee_email): def AppendAppointment(date, start_time, interviewee_name, interviewee_email):
""" """
Appends a new appointment with the interviewee's name and email if the slot is available. Appends a new appointment with the interviewee's name and email if the slot is available.
``REQUIRES``: date (str), start_time (str), interviewee_name (str), interviewee_email (str) ``REQUIRES``: ``str`` date, ``str`` start_time, ``str`` interviewee_name, ``str`` interviewee_email
``PROMISES``: Updates the Excel file with the new interviewee's name and email if there is an available slot. Returns Bool. ``PROMISES``: ``None`` Updates the Excel file with the new interviewee's name and email if there is an available slot. Returns Bool.
``Developed by``: Ahmad ``Developed by``: Ahmad, Brock
``Contact``: ahmad.ahmad1@ucalgary.ca ``Contact``: ahmad.ahmad1@ucalgary.ca, darkicewolf50@gmail.com
""" """
available_slots = ReadDatabase()
# Check if the requested slot is available in the `available_slots` structure year_donation = int(str(datetime.datetime.now().year)[2:]) + 1 # gets the last two digits of the current year then adds 1 for the current season
if date in available_slots and start_time in available_slots[date]: # name based off the 2025 naming system
with FileLock(lock_path): # Ensure process-safe access to the file file_path = f"OR{year_donation}-L-Interview Data.xlsx"
# Load workbook and select "Sheet1" for updating appointments lock_path = f"OR{year_donation}-L-Interview Data.xlsx.lock"
workbook = load_workbook(file_path)
sheet = workbook["Interview Timetable"]
df = pd.read_excel(file_path)
# Find and update the row that matches the provided date and start time # checks for if the file exisits for the year otherwise it will create one
for index, row in df.iterrows(): if not (os.path.isfile(file_path) or os.path.isfile(lock_path)):
row_date = str(row['Date']).split(" ")[0] NoSheet()
row_start_time = str(row['Start Time']) else:
if row_date == date and row_start_time == start_time: available_slots = ReadDatabase()
# Current entries for names and emails, and append new data with comma and space
current_names = str(row['Interviewee Name']).strip()
current_emails = str(row['Interviewee Email']).strip()
updated_names = f"{current_names}, {interviewee_name}" if current_names != "nan" else interviewee_name # Check if the requested slot is available in the `available_slots` structure
updated_emails = f"{current_emails}, {interviewee_email}" if current_emails != "nan" else interviewee_email if date in available_slots and start_time in available_slots[date]:
with FileLock(lock_path): # Ensure process-safe access to the file
# Load workbook and select "Sheet1" for updating appointments
workbook = load_workbook(file_path)
sheet = workbook["Interview Timetable"]
df = pd.read_excel(file_path)
# Update the cells with new names and emails # Find and update the row that matches the provided date and start time
name_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Name') + 1) for index, row in df.iterrows():
email_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Email') + 1) row_date = str(row['Date']).split(" ")[0]
name_cell.value = updated_names row_start_time = str(row['Start Time'])
email_cell.value = updated_emails
workbook.save(file_path) if row_date == date and row_start_time == start_time:
send_email(interviewee_email, interviewee_name, date, start_time) # Current entries for names and emails, and append new data with comma and space
return True current_names = str(row['Interviewee Name']).strip()
current_emails = str(row['Interviewee Email']).strip()
# If no slots available, return that the slot is unavailable updated_names = f"{current_names}, {interviewee_name}" if current_names != "nan" else interviewee_name
return False updated_emails = f"{current_emails}, {interviewee_email}" if current_emails != "nan" else interviewee_email
# Update the cells with new names and emails
name_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Name') + 1)
email_cell = sheet.cell(row=index + 2, column=df.columns.get_loc('Interviewee Email') + 1)
name_cell.value = updated_names
email_cell.value = updated_emails
workbook.save(file_path)
send_email(interviewee_email, interviewee_name, date, start_time)
return True
# If no slots available, return that the slot is unavailable
return False
def run_tests(): def run_tests():

Binary file not shown.

0
interviewPackagers.py Normal file
View File

View File

@ -35,7 +35,7 @@ def get_root():
) )
from GetSchedulePackager import getSchedulePackager from interviewPackagers import getSchedulePackager
@app.get("/getAppointments") @app.get("/getAppointments")
async def getAppointments(): async def getAppointments():
@ -65,7 +65,7 @@ async def getAppointments():
# status_code=200 commented out just to show how to change it if you wanted # status_code=200 commented out just to show how to change it if you wanted
) )
from postSelectAppointment import SelectAppointment from interviewPackagers import SelectAppointment
class Appointment(BaseModel): class Appointment(BaseModel):
""" """

View File

@ -1,44 +0,0 @@
from WriteDB import AppendAppointment
from email_validator import validate_email, EmailNotValidError
def SelectAppointment (appointmentJson):
"""
packages up a response for a http request
``appointmentJSON``: ``JSON``
The appointment date and time details
``returns``: ``json``
Returns the status of the booking confirmation
``Develop in part by``: Brock T
``Contact``: darkicewolf50@gmail.com
"""
"""
{
"intervieweeName": "Alice Johnson",
"date": "2024-09-16",
"startTime": "10:30:00",
"intervieweeEmail": "darkicewolf50@gmail.com"
}
"""
try:
validEmail = validate_email(appointmentJson["intervieweeEmail"], check_deliverability=True)
if validEmail:
status = AppendAppointment(date=appointmentJson["date"], start_time=appointmentJson["startTime"], interviewee_name=appointmentJson["intervieweeName"], interviewee_email=appointmentJson["intervieweeEmail"])
if status:
resBody = {"Success": True, "validEmail": "true"}
else:
resBody = {"Success": False, "validEmail": "true"}
# resBody["message"] = appointmentJson for testing
return resBody
except EmailNotValidError as e:
return {"Success": False, "validEmail": "false"}
if __name__ == "__main__":
print(SelectAppointment("10:00 AM"))

View File

@ -4,12 +4,20 @@ from email.mime.text import MIMEText
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz # For timezone handling import pytz # For timezone handling
def send_email(interviewee_email="darkicewolf50@gmail.com", interviewee_name="brock", date="10-1-2024", start_time="10:00 AM", location ="ENC25"): """
TODO add
"""
def send_email(interviewee_email="darkicewolf50@gmail.com", interviewee_name="brock", date="2024-1-10", start_time="10:00:00", location ="ENC25"):
""" """
Sends an email notification to the interviewee and a static Gmail account. Sends an email notification to the interviewee and to the uofcbaja account
``REQUIRES``: interviewee_email (str), interviewee_name (str), date (str), start_time (str) ``REQUIRES``: ``str`` interviewee_email, ``str`` interviewee_name, ``str`` date, ``str`` start_time
``PROMISES``: Sends an email to interviewee and static email on successful appointment booking.
``PROMISES``: ``EMAIL`` Sends an email to interviewee and static email on successful appointment booking.
``Developed by``: Ahmad
``Contact``: ahmad.ahmad1@ucalgary.ca
""" """
# Define static email for notifications and Gmail credentials # Define static email for notifications and Gmail credentials
static_email = "uofcbaja.noreply@gmail.com" static_email = "uofcbaja.noreply@gmail.com"

View File

@ -1,37 +0,0 @@
Recruitment Responses:
- Frist Name (What we should call them): Steve
- Last Name: the Bug
- Ucalgary Email: steve.the.bug@ucalgary.ca
- What Subsystem/SubTeam are you interested in?: |
Chassis
Ergonomics
Suspension
Steering
Powertrain
Final Drive
Any Mechanical
Business - Content Creation
Business - Business Relations
Software
- Major: General (1st Year)
- Academic Year: 1st
- Why are you interested in joining UCalgary BAJA?: Example Interest
- Where did you hear about us?: Testing
- Are you available for team meetings/work days? Saturdays 10 am - 4 pm: "Yes" #add condiftional formatting for no to make whole line red
Interview TimeTable:
- Date: 9/16/2024
- Meeting Duration: 30 min
- Start Time Slot: 10:00:00 AM
- Slot: 1
- Interviewee Name (What to call them): Steve
- Interviewee Email: steve.the.bug@ucalgary.ca
- Category (if not general): Test
- Interviewer(s) Name(s): Example
- Status: Dropdown (Options in datahelp) #default is Unknown
Data Helper:
- Status Dropdown:
- Unknown
- Done
- No Show
- Cancelled/Moved
- How to Add Dropdown: Go into data, click data validation, select list then select the area you want to get values from in the formula spot