linkedIn_auto_jobs_applier_with_AI
223 строки · 10.5 Кб
1import os2import re3import sys4from pathlib import Path5import yaml6import click7from selenium import webdriver8from selenium.webdriver.chrome.service import Service as ChromeService9from webdriver_manager.chrome import ChromeDriverManager10from selenium.common.exceptions import WebDriverException, TimeoutException11from lib_resume_builder_AIHawk import Resume,StyleManager,FacadeManager,ResumeGenerator12from src.utils import chromeBrowserOptions13from src.gpt import GPTAnswerer14from src.linkedIn_authenticator import LinkedInAuthenticator15from src.linkedIn_bot_facade import LinkedInBotFacade16from src.linkedIn_job_manager import LinkedInJobManager17from src.job_application_profile import JobApplicationProfile18
19# Suppress stderr
20sys.stderr = open(os.devnull, 'w')21
22class ConfigError(Exception):23pass24
25class ConfigValidator:26@staticmethod27def validate_email(email: str) -> bool:28return re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email) is not None29
30@staticmethod31def validate_yaml_file(yaml_path: Path) -> dict:32try:33with open(yaml_path, 'r') as stream:34return yaml.safe_load(stream)35except yaml.YAMLError as exc:36raise ConfigError(f"Error reading file {yaml_path}: {exc}")37except FileNotFoundError:38raise ConfigError(f"File not found: {yaml_path}")39
40
41def validate_config(config_yaml_path: Path) -> dict:42parameters = ConfigValidator.validate_yaml_file(config_yaml_path)43required_keys = {44'remote': bool,45'experienceLevel': dict,46'jobTypes': dict,47'date': dict,48'positions': list,49'locations': list,50'distance': int,51'companyBlacklist': list,52'titleBlacklist': list53}54
55for key, expected_type in required_keys.items():56if key not in parameters:57if key in ['companyBlacklist', 'titleBlacklist']:58parameters[key] = []59else:60raise ConfigError(f"Missing or invalid key '{key}' in config file {config_yaml_path}")61elif not isinstance(parameters[key], expected_type):62if key in ['companyBlacklist', 'titleBlacklist'] and parameters[key] is None:63parameters[key] = []64else:65raise ConfigError(f"Invalid type for key '{key}' in config file {config_yaml_path}. Expected {expected_type}.")66
67experience_levels = ['internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive']68for level in experience_levels:69if not isinstance(parameters['experienceLevel'].get(level), bool):70raise ConfigError(f"Experience level '{level}' must be a boolean in config file {config_yaml_path}")71
72job_types = ['full-time', 'contract', 'part-time', 'temporary', 'internship', 'other', 'volunteer']73for job_type in job_types:74if not isinstance(parameters['jobTypes'].get(job_type), bool):75raise ConfigError(f"Job type '{job_type}' must be a boolean in config file {config_yaml_path}")76
77date_filters = ['all time', 'month', 'week', '24 hours']78for date_filter in date_filters:79if not isinstance(parameters['date'].get(date_filter), bool):80raise ConfigError(f"Date filter '{date_filter}' must be a boolean in config file {config_yaml_path}")81
82if not all(isinstance(pos, str) for pos in parameters['positions']):83raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}")84if not all(isinstance(loc, str) for loc in parameters['locations']):85raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}")86
87approved_distances = {0, 5, 10, 25, 50, 100}88if parameters['distance'] not in approved_distances:89raise ConfigError(f"Invalid distance value in config file {config_yaml_path}. Must be one of: {approved_distances}")90
91for blacklist in ['companyBlacklist', 'titleBlacklist']:92if not isinstance(parameters.get(blacklist), list):93raise ConfigError(f"'{blacklist}' must be a list in config file {config_yaml_path}")94if parameters[blacklist] is None:95parameters[blacklist] = []96
97return parameters98
99
100
101@staticmethod102def validate_secrets(secrets_yaml_path: Path) -> tuple:103secrets = ConfigValidator.validate_yaml_file(secrets_yaml_path)104mandatory_secrets = ['email', 'password', 'openai_api_key']105
106for secret in mandatory_secrets:107if secret not in secrets:108raise ConfigError(f"Missing secret '{secret}' in file {secrets_yaml_path}")109
110if not ConfigValidator.validate_email(secrets['email']):111raise ConfigError(f"Invalid email format in secrets file {secrets_yaml_path}.")112if not secrets['password']:113raise ConfigError(f"Password cannot be empty in secrets file {secrets_yaml_path}.")114if not secrets['openai_api_key']:115raise ConfigError(f"OpenAI API key cannot be empty in secrets file {secrets_yaml_path}.")116
117return secrets['email'], str(secrets['password']), secrets['openai_api_key']118
119class FileManager:120@staticmethod121def find_file(name_containing: str, with_extension: str, at_path: Path) -> Path:122return next((file for file in at_path.iterdir() if name_containing.lower() in file.name.lower() and file.suffix.lower() == with_extension.lower()), None)123
124@staticmethod125def validate_data_folder(app_data_folder: Path) -> tuple:126if not app_data_folder.exists() or not app_data_folder.is_dir():127raise FileNotFoundError(f"Data folder not found: {app_data_folder}")128
129required_files = ['secrets.yaml', 'config.yaml', 'plain_text_resume.yaml']130missing_files = [file for file in required_files if not (app_data_folder / file).exists()]131
132if missing_files:133raise FileNotFoundError(f"Missing files in the data folder: {', '.join(missing_files)}")134
135output_folder = app_data_folder / 'output'136output_folder.mkdir(exist_ok=True)137return (app_data_folder / 'secrets.yaml', app_data_folder / 'config.yaml', app_data_folder / 'plain_text_resume.yaml', output_folder)138
139@staticmethod140def file_paths_to_dict(resume_file: Path | None, plain_text_resume_file: Path) -> dict:141if not plain_text_resume_file.exists():142raise FileNotFoundError(f"Plain text resume file not found: {plain_text_resume_file}")143
144result = {'plainTextResume': plain_text_resume_file}145
146if resume_file:147if not resume_file.exists():148raise FileNotFoundError(f"Resume file not found: {resume_file}")149result['resume'] = resume_file150
151return result152
153def init_browser() -> webdriver.Chrome:154try:155options = chromeBrowserOptions()156service = ChromeService(ChromeDriverManager().install())157return webdriver.Chrome(service=service, options=options)158except Exception as e:159raise RuntimeError(f"Failed to initialize browser: {str(e)}")160
161def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str):162try:163style_manager = StyleManager()164resume_generator = ResumeGenerator()165with open(parameters['uploads']['plainTextResume'], "r") as file:166plain_text_resume = file.read()167resume_object = Resume(plain_text_resume)168resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output"))169os.system('cls' if os.name == 'nt' else 'clear')170resume_generator_manager.choose_style()171os.system('cls' if os.name == 'nt' else 'clear')172
173job_application_profile_object = JobApplicationProfile(plain_text_resume)174
175browser = init_browser()176login_component = LinkedInAuthenticator(browser)177apply_component = LinkedInJobManager(browser)178gpt_answerer_component = GPTAnswerer(openai_api_key)179bot = LinkedInBotFacade(login_component, apply_component)180bot.set_secrets(email, password)181bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object)182bot.set_gpt_answerer_and_resume_generator(gpt_answerer_component, resume_generator_manager)183bot.set_parameters(parameters)184bot.start_login()185bot.start_apply()186except WebDriverException as e:187print(f"WebDriver error occurred: {e}")188except Exception as e:189raise RuntimeError(f"Error running the bot: {str(e)}")190
191
192@click.command()193@click.option('--resume', type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), help="Path to the resume PDF file")194def main(resume: Path = None):195try:196data_folder = Path("data_folder")197secrets_file, config_file, plain_text_resume_file, output_folder = FileManager.validate_data_folder(data_folder)198
199parameters = ConfigValidator.validate_config(config_file)200email, password, openai_api_key = ConfigValidator.validate_secrets(secrets_file)201
202parameters['uploads'] = FileManager.file_paths_to_dict(resume, plain_text_resume_file)203parameters['outputFileDirectory'] = output_folder204
205create_and_run_bot(email, password, parameters, openai_api_key)206except ConfigError as ce:207print(f"Configuration error: {str(ce)}")208print("Refer to the configuration guide for troubleshooting: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")209except FileNotFoundError as fnf:210print(f"File not found: {str(fnf)}")211print("Ensure all required files are present in the data folder.")212print("Refer to the file setup guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")213except RuntimeError as re:214
215print(f"Runtime error: {str(re)}")216
217print("Refer to the configuration and troubleshooting guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")218except Exception as e:219print(f"An unexpected error occurred: {str(e)}")220print("Refer to the general troubleshooting guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")221
222if __name__ == "__main__":223main()224