linkedIn_auto_jobs_applier_with_AI

Форк
0
223 строки · 10.5 Кб
1
import os
2
import re
3
import sys
4
from pathlib import Path
5
import yaml
6
import click
7
from selenium import webdriver
8
from selenium.webdriver.chrome.service import Service as ChromeService
9
from webdriver_manager.chrome import ChromeDriverManager
10
from selenium.common.exceptions import WebDriverException, TimeoutException
11
from lib_resume_builder_AIHawk import Resume,StyleManager,FacadeManager,ResumeGenerator
12
from src.utils import chromeBrowserOptions
13
from src.gpt import GPTAnswerer
14
from src.linkedIn_authenticator import LinkedInAuthenticator
15
from src.linkedIn_bot_facade import LinkedInBotFacade
16
from src.linkedIn_job_manager import LinkedInJobManager
17
from src.job_application_profile import JobApplicationProfile
18

19
# Suppress stderr
20
sys.stderr = open(os.devnull, 'w')
21

22
class ConfigError(Exception):
23
    pass
24

25
class ConfigValidator:
26
    @staticmethod
27
    def validate_email(email: str) -> bool:
28
        return re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email) is not None
29
    
30
    @staticmethod
31
    def validate_yaml_file(yaml_path: Path) -> dict:
32
        try:
33
            with open(yaml_path, 'r') as stream:
34
                return yaml.safe_load(stream)
35
        except yaml.YAMLError as exc:
36
            raise ConfigError(f"Error reading file {yaml_path}: {exc}")
37
        except FileNotFoundError:
38
            raise ConfigError(f"File not found: {yaml_path}")
39
    
40
    
41
    def validate_config(config_yaml_path: Path) -> dict:
42
        parameters = ConfigValidator.validate_yaml_file(config_yaml_path)
43
        required_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': list
53
        }
54

55
        for key, expected_type in required_keys.items():
56
            if key not in parameters:
57
                if key in ['companyBlacklist', 'titleBlacklist']:
58
                    parameters[key] = []
59
                else:
60
                    raise ConfigError(f"Missing or invalid key '{key}' in config file {config_yaml_path}")
61
            elif not isinstance(parameters[key], expected_type):
62
                if key in ['companyBlacklist', 'titleBlacklist'] and parameters[key] is None:
63
                    parameters[key] = []
64
                else:
65
                    raise ConfigError(f"Invalid type for key '{key}' in config file {config_yaml_path}. Expected {expected_type}.")
66

67
        experience_levels = ['internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive']
68
        for level in experience_levels:
69
            if not isinstance(parameters['experienceLevel'].get(level), bool):
70
                raise ConfigError(f"Experience level '{level}' must be a boolean in config file {config_yaml_path}")
71

72
        job_types = ['full-time', 'contract', 'part-time', 'temporary', 'internship', 'other', 'volunteer']
73
        for job_type in job_types:
74
            if not isinstance(parameters['jobTypes'].get(job_type), bool):
75
                raise ConfigError(f"Job type '{job_type}' must be a boolean in config file {config_yaml_path}")
76

77
        date_filters = ['all time', 'month', 'week', '24 hours']
78
        for date_filter in date_filters:
79
            if not isinstance(parameters['date'].get(date_filter), bool):
80
                raise ConfigError(f"Date filter '{date_filter}' must be a boolean in config file {config_yaml_path}")
81

82
        if not all(isinstance(pos, str) for pos in parameters['positions']):
83
            raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}")
84
        if not all(isinstance(loc, str) for loc in parameters['locations']):
85
            raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}")
86

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

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

97
        return parameters
98

99

100

101
    @staticmethod
102
    def validate_secrets(secrets_yaml_path: Path) -> tuple:
103
        secrets = ConfigValidator.validate_yaml_file(secrets_yaml_path)
104
        mandatory_secrets = ['email', 'password', 'openai_api_key']
105

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

110
        if not ConfigValidator.validate_email(secrets['email']):
111
            raise ConfigError(f"Invalid email format in secrets file {secrets_yaml_path}.")
112
        if not secrets['password']:
113
            raise ConfigError(f"Password cannot be empty in secrets file {secrets_yaml_path}.")
114
        if not secrets['openai_api_key']:
115
            raise ConfigError(f"OpenAI API key cannot be empty in secrets file {secrets_yaml_path}.")
116

117
        return secrets['email'], str(secrets['password']), secrets['openai_api_key']
118

119
class FileManager:
120
    @staticmethod
121
    def find_file(name_containing: str, with_extension: str, at_path: Path) -> Path:
122
        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)
123

124
    @staticmethod
125
    def validate_data_folder(app_data_folder: Path) -> tuple:
126
        if not app_data_folder.exists() or not app_data_folder.is_dir():
127
            raise FileNotFoundError(f"Data folder not found: {app_data_folder}")
128

129
        required_files = ['secrets.yaml', 'config.yaml', 'plain_text_resume.yaml']
130
        missing_files = [file for file in required_files if not (app_data_folder / file).exists()]
131
        
132
        if missing_files:
133
            raise FileNotFoundError(f"Missing files in the data folder: {', '.join(missing_files)}")
134

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

139
    @staticmethod
140
    def file_paths_to_dict(resume_file: Path | None, plain_text_resume_file: Path) -> dict:
141
        if not plain_text_resume_file.exists():
142
            raise FileNotFoundError(f"Plain text resume file not found: {plain_text_resume_file}")
143

144
        result = {'plainTextResume': plain_text_resume_file}
145

146
        if resume_file:
147
            if not resume_file.exists():
148
                raise FileNotFoundError(f"Resume file not found: {resume_file}")
149
            result['resume'] = resume_file
150

151
        return result
152

153
def init_browser() -> webdriver.Chrome:
154
    try:
155
        options = chromeBrowserOptions()
156
        service = ChromeService(ChromeDriverManager().install())
157
        return webdriver.Chrome(service=service, options=options)
158
    except Exception as e:
159
        raise RuntimeError(f"Failed to initialize browser: {str(e)}")
160

161
def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str):
162
    try:
163
        style_manager = StyleManager()
164
        resume_generator = ResumeGenerator()
165
        with open(parameters['uploads']['plainTextResume'], "r") as file:
166
            plain_text_resume = file.read()
167
        resume_object = Resume(plain_text_resume)
168
        resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output"))
169
        os.system('cls' if os.name == 'nt' else 'clear')
170
        resume_generator_manager.choose_style()
171
        os.system('cls' if os.name == 'nt' else 'clear')
172
        
173
        job_application_profile_object = JobApplicationProfile(plain_text_resume)
174
        
175
        browser = init_browser()
176
        login_component = LinkedInAuthenticator(browser)
177
        apply_component = LinkedInJobManager(browser)
178
        gpt_answerer_component = GPTAnswerer(openai_api_key)
179
        bot = LinkedInBotFacade(login_component, apply_component)
180
        bot.set_secrets(email, password)
181
        bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object)
182
        bot.set_gpt_answerer_and_resume_generator(gpt_answerer_component, resume_generator_manager)
183
        bot.set_parameters(parameters)
184
        bot.start_login()
185
        bot.start_apply()
186
    except WebDriverException as e:
187
        print(f"WebDriver error occurred: {e}")
188
    except Exception as e:
189
        raise 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")
194
def main(resume: Path = None):
195
    try:
196
        data_folder = Path("data_folder")
197
        secrets_file, config_file, plain_text_resume_file, output_folder = FileManager.validate_data_folder(data_folder)
198
        
199
        parameters = ConfigValidator.validate_config(config_file)
200
        email, password, openai_api_key = ConfigValidator.validate_secrets(secrets_file)
201
        
202
        parameters['uploads'] = FileManager.file_paths_to_dict(resume, plain_text_resume_file)
203
        parameters['outputFileDirectory'] = output_folder
204
        
205
        create_and_run_bot(email, password, parameters, openai_api_key)
206
    except ConfigError as ce:
207
        print(f"Configuration error: {str(ce)}")
208
        print("Refer to the configuration guide for troubleshooting: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
209
    except FileNotFoundError as fnf:
210
        print(f"File not found: {str(fnf)}")
211
        print("Ensure all required files are present in the data folder.")
212
        print("Refer to the file setup guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
213
    except RuntimeError as re:
214

215
        print(f"Runtime error: {str(re)}")
216

217
        print("Refer to the configuration and troubleshooting guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
218
    except Exception as e:
219
        print(f"An unexpected error occurred: {str(e)}")
220
        print("Refer to the general troubleshooting guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
221

222
if __name__ == "__main__":
223
    main()
224

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.