first commit v2
This commit is contained in:
parent
2dcd7943e8
commit
d8b7e7fda6
18 changed files with 663 additions and 1398 deletions
191
main.py
191
main.py
|
|
@ -1,113 +1,104 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
import click
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service as ChromeService
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
import click
|
||||
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||
|
||||
sys.path.append('C:\\Users\\loren\\OneDrive\\Desktop\\Nuovacartella\\LinkedIn-GPT-EasyApplyBot-master\\LinkedIn_AIHawk_automatic_job_application\\lib_resume_builder_AIHawk')
|
||||
|
||||
from lib_resume_builder_AIHawk import Resume,StyleManager,FacadeManager,ResumeGenerator
|
||||
from utils import chromeBrowserOptions
|
||||
from gpt import GPTAnswerer
|
||||
from linkedIn_authenticator import LinkedInAuthenticator
|
||||
from linkedIn_bot_facade import LinkedInBotFacade
|
||||
from linkedIn_job_manager import LinkedInJobManager
|
||||
from resume import Resume
|
||||
from job_application_profile import JobApplicationProfile
|
||||
|
||||
# Suppress stderr
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""Custom exception for configuration errors."""
|
||||
pass
|
||||
|
||||
class ConfigValidator:
|
||||
@staticmethod
|
||||
def validate_email(email: str) -> bool:
|
||||
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
return re.match(email_regex, email) is not None
|
||||
return re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email) is not None
|
||||
|
||||
@staticmethod
|
||||
def validate_yaml_file(yaml_path: Path) -> dict:
|
||||
try:
|
||||
with open(yaml_path, 'r') as stream:
|
||||
return yaml.safe_load(stream)
|
||||
except yaml.YAMLError as exc:
|
||||
raise ConfigError(f"Error reading file {yaml_path}: {exc}")
|
||||
except FileNotFoundError:
|
||||
raise ConfigError(f"File not found: {yaml_path}")
|
||||
|
||||
@staticmethod
|
||||
def validate_config(config_yaml_path: Path) -> dict:
|
||||
try:
|
||||
with open(config_yaml_path, 'r') as stream:
|
||||
parameters = yaml.safe_load(stream)
|
||||
except yaml.YAMLError as exc:
|
||||
raise ConfigError(f"Error reading config file {config_yaml_path}: {exc}")
|
||||
except FileNotFoundError:
|
||||
raise ConfigError(f"Config file not found: {config_yaml_path}")
|
||||
parameters = ConfigValidator.validate_yaml_file(config_yaml_path)
|
||||
|
||||
required_keys = {
|
||||
'remote': bool,
|
||||
'experienceLevel': dict,
|
||||
'jobTypes': dict,
|
||||
'date': dict,
|
||||
'positions': list,
|
||||
'locations': list,
|
||||
'distance': int,
|
||||
'companyBlacklist': list,
|
||||
'titleBlacklist': list
|
||||
}
|
||||
|
||||
for key, expected_type in required_keys.items():
|
||||
if key not in parameters or not isinstance(parameters[key], expected_type):
|
||||
raise ConfigError(f"Missing or invalid key '{key}' in config file {config_yaml_path}")
|
||||
|
||||
# Validate 'remote'
|
||||
if 'remote' not in parameters or not isinstance(parameters['remote'], bool):
|
||||
raise ConfigError(f"'remote' in config file {config_yaml_path} must be a boolean value.")
|
||||
experience_levels = ['internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive']
|
||||
for level in experience_levels:
|
||||
if not isinstance(parameters['experienceLevel'].get(level), bool):
|
||||
raise ConfigError(f"Experience level '{level}' must be a boolean in config file {config_yaml_path}")
|
||||
|
||||
# Validate 'experienceLevel'
|
||||
experience_level = parameters.get('experienceLevel', {})
|
||||
valid_experience_levels = [
|
||||
'internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive'
|
||||
]
|
||||
for level in valid_experience_levels:
|
||||
if level not in experience_level or not isinstance(experience_level[level], bool):
|
||||
raise ConfigError(f"Experience level '{level}' must be a boolean value in config file {config_yaml_path}.")
|
||||
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):
|
||||
raise ConfigError(f"Job type '{job_type}' must be a boolean in config file {config_yaml_path}")
|
||||
|
||||
# Validate 'jobTypes'
|
||||
job_types = parameters.get('jobTypes', {})
|
||||
valid_job_types = [
|
||||
'full-time', 'contract', 'part-time', 'temporary', 'internship', 'other', 'volunteer'
|
||||
]
|
||||
for job_type in valid_job_types:
|
||||
if job_type not in job_types or not isinstance(job_types[job_type], bool):
|
||||
raise ConfigError(f"Job type '{job_type}' must be a boolean value in config file {config_yaml_path}.")
|
||||
date_filters = ['all time', 'month', 'week', '24 hours']
|
||||
for date_filter in date_filters:
|
||||
if not isinstance(parameters['date'].get(date_filter), bool):
|
||||
raise ConfigError(f"Date filter '{date_filter}' must be a boolean in config file {config_yaml_path}")
|
||||
|
||||
# Validate 'date'
|
||||
date = parameters.get('date', {})
|
||||
valid_dates = ['all time', 'month', 'week', '24 hours']
|
||||
for date_filter in valid_dates:
|
||||
if date_filter not in date or not isinstance(date[date_filter], bool):
|
||||
raise ConfigError(f"Date filter '{date_filter}' must be a boolean value in config file {config_yaml_path}.")
|
||||
if not all(isinstance(pos, str) for pos in parameters['positions']):
|
||||
raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}")
|
||||
if not all(isinstance(loc, str) for loc in parameters['locations']):
|
||||
raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}")
|
||||
|
||||
# Validate 'positions'
|
||||
positions = parameters.get('positions', [])
|
||||
if not isinstance(positions, list) or not all(isinstance(pos, str) for pos in positions):
|
||||
raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}.")
|
||||
|
||||
# Validate 'locations'
|
||||
locations = parameters.get('locations', [])
|
||||
if not isinstance(locations, list) or not all(isinstance(loc, str) for loc in locations):
|
||||
raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}.")
|
||||
|
||||
# Validate 'distance'
|
||||
approved_distances = {0, 5, 10, 25, 50, 100}
|
||||
distance = parameters.get('distance')
|
||||
if distance not in approved_distances:
|
||||
if parameters['distance'] not in approved_distances:
|
||||
raise ConfigError(f"Invalid distance value in config file {config_yaml_path}. Must be one of: {approved_distances}")
|
||||
|
||||
# Validate 'companyBlacklist'
|
||||
company_blacklist = parameters.get('companyBlacklist', [])
|
||||
if not isinstance(company_blacklist, list) or not all(isinstance(comp, str) for comp in company_blacklist):
|
||||
company_blacklist = []
|
||||
parameters['companyBlacklist'] = company_blacklist
|
||||
for blacklist in ['companyBlacklist', 'titleBlacklist']:
|
||||
if not all(isinstance(item, str) for item in parameters.get(blacklist, [])):
|
||||
parameters[blacklist] = []
|
||||
|
||||
# Validate 'titleBlacklist'
|
||||
title_blacklist = parameters.get('titleBlacklist', [])
|
||||
if not isinstance(title_blacklist, list) or not all(isinstance(word, str) for word in title_blacklist):
|
||||
title_blacklist = []
|
||||
parameters['titleBlacklist'] = title_blacklist
|
||||
return parameters
|
||||
|
||||
@staticmethod
|
||||
def validate_secrets(secrets_yaml_path: Path) -> tuple:
|
||||
try:
|
||||
with open(secrets_yaml_path, 'r') as stream:
|
||||
secrets = yaml.safe_load(stream)
|
||||
except yaml.YAMLError as exc:
|
||||
raise ConfigError(f"Error reading secrets file {secrets_yaml_path}: {exc}")
|
||||
except FileNotFoundError:
|
||||
raise ConfigError(f"Secrets file not found: {secrets_yaml_path}")
|
||||
|
||||
secrets = ConfigValidator.validate_yaml_file(secrets_yaml_path)
|
||||
mandatory_secrets = ['email', 'password', 'openai_api_key']
|
||||
|
||||
for secret in mandatory_secrets:
|
||||
if secret not in secrets:
|
||||
raise ConfigError(f"Missing secret in file {secrets_yaml_path}: {secret}")
|
||||
|
||||
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']:
|
||||
|
|
@ -120,48 +111,38 @@ class ConfigValidator:
|
|||
class FileManager:
|
||||
@staticmethod
|
||||
def find_file(name_containing: str, with_extension: str, at_path: Path) -> Path:
|
||||
for file in at_path.iterdir():
|
||||
if name_containing.lower() in file.name.lower() and file.suffix.lower() == with_extension.lower():
|
||||
return file
|
||||
return 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:
|
||||
if not app_data_folder.exists() or not app_data_folder.is_dir():
|
||||
raise FileNotFoundError(f"Data folder not found: {app_data_folder}")
|
||||
|
||||
secrets_file = app_data_folder / 'secrets.yaml'
|
||||
config_file = app_data_folder / 'config.yaml'
|
||||
plain_text_resume_file = app_data_folder / 'plain_text_resume.yaml'
|
||||
|
||||
missing_files = []
|
||||
if not config_file.exists():
|
||||
missing_files.append('config.yaml')
|
||||
if not plain_text_resume_file.exists():
|
||||
missing_files.append('plain_text_resume.yaml')
|
||||
required_files = ['secrets.yaml', 'config.yaml', 'plain_text_resume.yaml']
|
||||
missing_files = [file for file in required_files if not (app_data_folder / file).exists()]
|
||||
|
||||
if missing_files:
|
||||
raise FileNotFoundError(f"Missing files in the data folder: {', '.join(missing_files)}")
|
||||
|
||||
|
||||
output_folder = app_data_folder / 'output'
|
||||
output_folder.mkdir(exist_ok=True)
|
||||
return secrets_file, config_file, plain_text_resume_file, 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:
|
||||
if not plain_text_resume_file.exists():
|
||||
raise FileNotFoundError(f"Plain text resume file not found: {plain_text_resume_file}")
|
||||
|
||||
|
||||
result = {'plainTextResume': plain_text_resume_file}
|
||||
|
||||
if resume_file is not None:
|
||||
|
||||
if resume_file:
|
||||
if not resume_file.exists():
|
||||
raise FileNotFoundError(f"Resume file not found: {resume_file}")
|
||||
result['resume'] = resume_file
|
||||
|
||||
|
||||
return result
|
||||
|
||||
def init_browser():
|
||||
def init_browser() -> webdriver.Chrome:
|
||||
try:
|
||||
options = chromeBrowserOptions()
|
||||
service = ChromeService(ChromeDriverManager().install())
|
||||
|
|
@ -172,33 +153,50 @@ def init_browser():
|
|||
def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str):
|
||||
try:
|
||||
browser = init_browser()
|
||||
|
||||
login_component = LinkedInAuthenticator(browser)
|
||||
apply_component = LinkedInJobManager(browser)
|
||||
gpt_answerer_component = GPTAnswerer(openai_api_key)
|
||||
|
||||
with open(parameters['uploads']['plainTextResume'], "r") as file:
|
||||
plain_text_resume_file = file.read()
|
||||
resume_object = Resume(plain_text_resume_file)
|
||||
plain_text_resume = file.read()
|
||||
|
||||
resume_object = Resume(plain_text_resume)
|
||||
job_application_profile_object = JobApplicationProfile(plain_text_resume)
|
||||
|
||||
style_manager = StyleManager()
|
||||
resume_generator = ResumeGenerator()
|
||||
resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output"))
|
||||
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
resume_generator_manager.choose_style()
|
||||
|
||||
bot = LinkedInBotFacade(login_component, apply_component)
|
||||
bot.set_secrets(email, password)
|
||||
bot.set_resume(resume_object)
|
||||
bot.set_gpt_answerer(gpt_answerer_component)
|
||||
bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object)
|
||||
bot.set_gpt_answerer_and_resume_generator(gpt_answerer_component, resume_generator_manager)
|
||||
bot.set_parameters(parameters)
|
||||
bot.start_login()
|
||||
bot.start_apply()
|
||||
except WebDriverException as e:
|
||||
print(f"WebDriver error occurred: {e}")
|
||||
except Exception as 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")
|
||||
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)
|
||||
email, password, openai_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(email, password, parameters, openai_api_key)
|
||||
except ConfigError as ce:
|
||||
print(f"Configuration error: {str(ce)}")
|
||||
|
|
@ -208,8 +206,9 @@ def main(resume: Path = None):
|
|||
print("Ensure all required files are present in the data folder.")
|
||||
print("Refer to the file setup guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
|
||||
except RuntimeError as re:
|
||||
|
||||
print(f"Runtime error: {str(re)}")
|
||||
print("Check browser setup and other runtime issues.")
|
||||
|
||||
print("Refer to the configuration and troubleshooting guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {str(e)}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue