Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes made. #473

Merged
merged 27 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8176711
Update job_application_profile.py
queukat Oct 4, 2024
5f53924
Update aihawk_easy_applier.py
queukat Oct 4, 2024
b7ca236
Update aihawk_job_manager.py
queukat Oct 4, 2024
82a53ce
Update utils.py
queukat Oct 4, 2024
5d3d780
Update aihawk_authenticator.py
queukat Oct 4, 2024
7c91d7a
Update strings.py
queukat Oct 4, 2024
853d6ce
Update llm_manager.py
queukat Oct 4, 2024
d95999d
Update aihawk_bot_facade.py
queukat Oct 4, 2024
bd3a6f5
Update test_aihawk_job_manager.py
queukat Oct 4, 2024
2474660
Update test_aihawk_easy_applier.py
queukat Oct 4, 2024
029bbdb
Update test_aihawk_easy_applier.py
queukat Oct 4, 2024
48e1aa1
Update test_job_application_profile.py
queukat Oct 4, 2024
261cae1
Update test_aihawk_easy_applier.py
queukat Oct 4, 2024
63f69c0
Update test_aihawk_easy_applier.py
queukat Oct 4, 2024
bf35f46
Update test_aihawk_job_manager.py
queukat Oct 4, 2024
66245e9
Update test_job_application_profile.py
queukat Oct 4, 2024
d47f26f
Update test_aihawk_job_manager.py
queukat Oct 4, 2024
afd63a9
Update test_aihawk_job_manager.py
queukat Oct 4, 2024
537344c
Update aihawk_job_manager.py
queukat Oct 4, 2024
e035819
Update main.py
queukat Oct 5, 2024
0d932d6
Update requirements.txt
queukat Oct 5, 2024
d81fc04
Update aihawk_authenticator.py
queukat Oct 5, 2024
aabd02a
Update aihawk_easy_applier.py
queukat Oct 5, 2024
d586463
Update main.py
queukat Oct 5, 2024
9278161
Update aihawk_job_manager.py
queukat Oct 5, 2024
d45f5e3
Update aihawk_easy_applier.py
queukat Oct 5, 2024
5e990ac
Update requirements.txt
queukat Oct 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 145 additions & 27 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
# Suppress stderr
sys.stderr = open(os.devnull, 'w')


class ConfigError(Exception):
pass


class ConfigValidator:
@staticmethod
def validate_email(email: str) -> bool:
Expand All @@ -38,86 +40,137 @@ def validate_yaml_file(yaml_path: Path) -> dict:
except FileNotFoundError:
raise ConfigError(f"File not found: {yaml_path}")


def validate_config(config_yaml_path: Path) -> dict:
"""
Validate and load configuration from a YAML file.

This function validates the configuration file to ensure that all required keys are present
and that the types of their values are correct. It checks for required sections like 'remote',
'experience_level', 'jobTypes', 'positions', 'locations', and 'personal_information', among others.
If any key is missing or has an invalid type, it raises a ConfigError.

Args:
config_yaml_path (Path): Path to the YAML configuration file.

Returns:
dict: A dictionary containing the validated configuration parameters.

Raises:
ConfigError: If a required key is missing or has an invalid type.
"""

logger.info(f"Loading and validating config file: {config_yaml_path}")

# Load the YAML configuration file
parameters = ConfigValidator.validate_yaml_file(config_yaml_path)
logger.debug(f"Loaded config file content: {parameters}")

# Define required keys and their expected types
required_keys = {
'remote': bool,
'experienceLevel': dict,
'experience_level': dict,
'jobTypes': dict,
'date': dict,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats this date? shouldn't it be in str, dict, tuple format?

'positions': list,
'locations': list,
'distance': int,
'companyBlacklist': list,
'titleBlacklist': list,
'company_blacklist': list,
'title_blacklist': list,
'llm_model_type': str,
'llm_model': str

}

# Validate each required key
for key, expected_type in required_keys.items():
if key not in parameters:
if key in ['companyBlacklist', 'titleBlacklist']:
# Handle optional blacklist keys by setting them as empty lists if missing
if key in ['company_blacklist', 'title_blacklist']:
parameters[key] = []
logger.debug(f"Optional key '{key}' missing, set to empty list.")
else:
logger.error(f"Missing key '{key}' in config file.")
raise ConfigError(f"Missing or invalid key '{key}' in config file {config_yaml_path}")
elif not isinstance(parameters[key], expected_type):
if key in ['companyBlacklist', 'titleBlacklist'] and parameters[key] is None:
# Allow None values for blacklists, but otherwise check type validity
if key in ['company_blacklist', 'title_blacklist'] and parameters[key] is None:
parameters[key] = []
logger.debug(f"Key '{key}' was None, set to empty list.")
else:
raise ConfigError(f"Invalid type for key '{key}' in config file {config_yaml_path}. Expected {expected_type}.")
logger.error(f"Invalid type for key '{key}' in config file. Expected {expected_type}, but got {type(parameters[key])}.")
raise ConfigError(f"Invalid type for key '{key}' in config file {config_yaml_path}. Expected {expected_type}, but got {type(parameters[key])}.")

# Validate 'experience_level' section
experience_levels = ['internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive']
for level in experience_levels:
if not isinstance(parameters['experienceLevel'].get(level), bool):
if not isinstance(parameters['experience_level'].get(level), bool):
logger.error(f"Invalid value for experience level '{level}'. Expected a boolean (True/False). Current value: {parameters['experience_level'].get(level)}")
raise ConfigError(f"Experience level '{level}' must be a boolean in config file {config_yaml_path}")

# Validate 'jobTypes' section
job_types = ['full-time', 'contract', 'part-time', 'temporary', 'internship', 'other', 'volunteer']
for job_type in job_types:
if not isinstance(parameters['jobTypes'].get(job_type), bool):
logger.error(f"Invalid value for job type '{job_type}'. Expected a boolean (True/False). Current value: {parameters['jobTypes'].get(job_type)}")
raise ConfigError(f"Job type '{job_type}' must be a boolean in config file {config_yaml_path}")

# Validate 'date' filters section
date_filters = ['all time', 'month', 'week', '24 hours']
for date_filter in date_filters:
if not isinstance(parameters['date'].get(date_filter), bool):
logger.error(f"Invalid value for date filter '{date_filter}'. Expected a boolean (True/False). Current value: {parameters['date'].get(date_filter)}")
raise ConfigError(f"Date filter '{date_filter}' must be a boolean in config file {config_yaml_path}")

# Validate 'positions' list
if not all(isinstance(pos, str) for pos in parameters['positions']):
logger.error(f"Invalid value in 'positions'. All entries must be strings. Current values: {parameters['positions']}")
raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}")

# Validate 'locations' list
if not all(isinstance(loc, str) for loc in parameters['locations']):
logger.error(f"Invalid value in 'locations'. All entries must be strings. Current values: {parameters['locations']}")
raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}")

# Validate 'distance' field
approved_distances = {0, 5, 10, 25, 50, 100}
if parameters['distance'] not in approved_distances:
logger.error(f"Invalid distance value '{parameters['distance']}'. Must be one of {approved_distances}.")
raise ConfigError(f"Invalid distance value in config file {config_yaml_path}. Must be one of: {approved_distances}")

for blacklist in ['companyBlacklist', 'titleBlacklist']:
for blacklist in ['company_blacklist', 'title_blacklist']:
if not isinstance(parameters.get(blacklist), list):
raise ConfigError(f"'{blacklist}' must be a list in config file {config_yaml_path}")
if parameters[blacklist] is None:
parameters[blacklist] = []

return parameters

logger.info("Configuration validated successfully.")

return parameters

@staticmethod
def validate_secrets(secrets_yaml_path: Path) -> tuple:
secrets = ConfigValidator.validate_yaml_file(secrets_yaml_path)
mandatory_secrets = ['llm_api_key']
mandatory_secrets = ['email', 'password','llm_api_key']

for secret in mandatory_secrets:
if secret not in secrets:
raise ConfigError(f"Missing secret '{secret}' in file {secrets_yaml_path}")

if not ConfigValidator.validate_email(secrets['email']):
raise ConfigError(f"Invalid email format in secrets file {secrets_yaml_path}.")
if not secrets['password']:
raise ConfigError(f"Password cannot be empty in secrets file {secrets_yaml_path}.")
return secrets['email'], str(secrets['password']), secrets['llm_api_key']
if not secrets['llm_api_key']:
raise ConfigError(f"llm_api_key cannot be empty in secrets file {secrets_yaml_path}.")
return secrets['llm_api_key']

class FileManager:
@staticmethod
def find_file(name_containing: str, with_extension: str, at_path: Path) -> Path:
return next((file for file in at_path.iterdir() if name_containing.lower() in file.name.lower() and file.suffix.lower() == with_extension.lower()), None)
return next((file for file in at_path.iterdir() if
name_containing.lower() in file.name.lower() and file.suffix.lower() == with_extension.lower()),
None)

@staticmethod
def validate_data_folder(app_data_folder: Path) -> tuple:
Expand All @@ -132,7 +185,9 @@ def validate_data_folder(app_data_folder: Path) -> tuple:

output_folder = app_data_folder / 'output'
output_folder.mkdir(exist_ok=True)
return (app_data_folder / 'secrets.yaml', app_data_folder / 'config.yaml', app_data_folder / 'plain_text_resume.yaml', output_folder)
return (
app_data_folder / 'secrets.yaml', app_data_folder / 'config.yaml', app_data_folder / 'plain_text_resume.yaml',
output_folder)

@staticmethod
def file_paths_to_dict(resume_file: Path | None, plain_text_resume_file: Path) -> dict:
Expand All @@ -148,62 +203,125 @@ def file_paths_to_dict(resume_file: Path | None, plain_text_resume_file: Path) -

return result


def init_browser() -> webdriver.Chrome:
try:

options = chrome_browser_options()
service = ChromeService(ChromeDriverManager().install())
return webdriver.Chrome(service=service, options=options)
except Exception as e:
raise RuntimeError(f"Failed to initialize browser: {str(e)}")

def create_and_run_bot(parameters, llm_api_key):

def create_and_run_bot(email, password, parameters, llm_api_key):
try:
logger.info("Starting bot initialization...")
logger.debug(f"Email: {email}")
logger.debug(f"Parameters: {parameters}")
logger.debug(f"LLM API Key: {llm_api_key}")

style_manager = StyleManager()
logger.debug("StyleManager initialized successfully.")

resume_generator = ResumeGenerator()
logger.debug("ResumeGenerator initialized successfully.")

logger.info("Reading plain text resume file...")
with open(parameters['uploads']['plainTextResume'], "r", encoding='utf-8') as file:
plain_text_resume = file.read()
logger.debug(f"Plain text resume loaded: {plain_text_resume[:100]}...") # Логируем первые 100 символов

resume_object = Resume(plain_text_resume)
resume_generator_manager = FacadeManager(llm_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output"))
logger.debug(f"Resume object created: {resume_object}")

# generate resume only if no resume flag was provided
if "resume" not in parameters["uploads"]:
os.system("cls" if os.name == "nt" else "clear")
resume_generator_manager.choose_style()
os.system("cls" if os.name == "nt" else "clear")
logger.info("Creating FacadeManager for resume generation...")
resume_generator_manager = FacadeManager(
llm_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output")
)
logger.debug("FacadeManager initialized successfully.")

os.system('cls' if os.name == 'nt' else 'clear')
logger.info("Choosing resume style...")
resume_generator_manager.choose_style()
logger.info("Resume style chosen successfully.")
os.system('cls' if os.name == 'nt' else 'clear')

logger.info("Creating JobApplicationProfile object...")
job_application_profile_object = JobApplicationProfile(plain_text_resume)
logger.debug(f"JobApplicationProfile created: {job_application_profile_object}")

logger.info("Initializing the browser...")
browser = init_browser()
login_component = AIHawkAuthenticator(browser)
logger.debug("Browser initialized successfully.")

logger.info("Initializing job application component...")
apply_component = AIHawkJobManager(browser)
logger.debug(f"Job application component created: {apply_component}")

logger.info("Creating AIHawkBotFacade object...")
bot = AIHawkBotFacade(None, apply_component)
logger.debug(f"Bot facade created: {bot}")

logger.info("Setting secrets for the bot...")
bot.set_secrets(email, password)
logger.info("Secrets set successfully.")

logger.info("Initializing login component...")
login_component = AIHawkAuthenticator(driver=browser, bot_facade=bot)
logger.debug(f"Login component created: {login_component}")

bot.login_component = login_component
logger.debug("Login component set in bot facade.")

logger.info("Initializing GPT Answerer component...")
gpt_answerer_component = GPTAnswerer(parameters, llm_api_key)
bot = AIHawkBotFacade(login_component, apply_component)
logger.debug(f"GPT Answerer component created: {gpt_answerer_component}")


logger.info("Setting job application profile and resume...")
bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object)
logger.info("Job application profile and resume set successfully.")

logger.info("Setting GPT Answerer and resume generator...")
bot.set_gpt_answerer_and_resume_generator(gpt_answerer_component, resume_generator_manager)
logger.info("GPT Answerer and resume generator set successfully.")

logger.info("Setting additional parameters for the bot...")
bot.set_parameters(parameters)
logger.info("Parameters set successfully.")

logger.info("Starting bot login process...")
bot.start_login()
logger.info("Login process completed successfully.")

logger.info("Starting job application process...")
bot.start_apply()
logger.info("Job application process completed successfully.")

except WebDriverException as e:
logger.error(f"WebDriver error occurred: {e}")
except Exception as e:
logger.error("An unexpected error occurred in create_and_run_bot:")
logger.exception(e)
raise RuntimeError(f"Error running the bot: {str(e)}")



@click.command()
@click.option('--resume', type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), help="Path to the resume PDF file")
@click.option('--resume', type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
help="Path to the resume PDF file")
def main(resume: Path = None):
try:
data_folder = Path("data_folder")
secrets_file, config_file, plain_text_resume_file, output_folder = FileManager.validate_data_folder(data_folder)

parameters = ConfigValidator.validate_config(config_file)
llm_api_key = ConfigValidator.validate_secrets(secrets_file)
email, password, llm_api_key = ConfigValidator.validate_secrets(secrets_file)

parameters['uploads'] = FileManager.file_paths_to_dict(resume, plain_text_resume_file)
parameters['outputFileDirectory'] = output_folder

create_and_run_bot(parameters, llm_api_key)
create_and_run_bot(email, password, parameters, llm_api_key)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main has too many unnecessary logs, and its making code unreadable. The raise error is sufficient in all the case.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use wrapper if we need to keep so many debug logs?

class LoggerWrapper:
    def __init__(self, logger):
        self.logger = logger
        self.debug_mode = os.environ.get('DEBUG', 'False').lower() == 'true'

    def debug(self, message, *args, **kwargs):
        if self.debug_mode:
            self.logger.debug(message, *args, **kwargs)

    def info(self, message, *args, **kwargs):
        self.logger.info(message, *args, **kwargs)

    def __getattr__(self, name):
        return getattr(self.logger, name)

except ConfigError as ce:
logger.error(f"Configuration error: {str(ce)}")
logger.error(f"Refer to the configuration guide for troubleshooting: https://github.com/feder-cr/AIHawk_AIHawk_automatic_job_application/blob/main/readme.md#configuration {str(ce)}")
Expand Down
Loading
Loading