From d8b7e7fda6e390316b6d8d2f4bdb07c9b0cc4b1f Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 10:42:49 +0100 Subject: [PATCH 01/23] first commit v2 --- .gitignore | 3 +- data_folder_example/config.yaml | 38 --- data_folder_example/plain_text_resume.yaml | 128 -------- data_folder_example/secrets.yaml | 3 - gpt.py | 179 +++++++---- job.py | 1 + job_application_profile.py | 132 ++++++++ linkedIn_bot_facade.py | 90 +++--- linkedIn_easy_applier.py | 345 ++++++++------------- linkedIn_job_manager.py | 78 +++-- main.py | 191 ++++++------ requirements.txt | Bin 566 -> 834 bytes resume.py | 127 -------- resume_template/casual_markdown.js | 202 ------------ resume_template/reorganizeHeader.js | 43 --- resume_template/resume.css | 156 ---------- strings.py | 240 ++++---------- utils.py | 105 +++---- 18 files changed, 663 insertions(+), 1398 deletions(-) delete mode 100644 data_folder_example/config.yaml delete mode 100644 data_folder_example/plain_text_resume.yaml delete mode 100644 data_folder_example/secrets.yaml create mode 100644 job_application_profile.py delete mode 100644 resume.py delete mode 100644 resume_template/casual_markdown.js delete mode 100644 resume_template/reorganizeHeader.js delete mode 100644 resume_template/resume.css diff --git a/.gitignore b/.gitignore index 26350fb..2d34995 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ data_folder* generated_cv* resume.html .vscode -chrome_profile \ No newline at end of file +chrome_profile +lib* \ No newline at end of file diff --git a/data_folder_example/config.yaml b/data_folder_example/config.yaml deleted file mode 100644 index 0de12f6..0000000 --- a/data_folder_example/config.yaml +++ /dev/null @@ -1,38 +0,0 @@ -remote: true - -experienceLevel: - internship: true - entry: false - associate: true - mid-senior level: false - director: true - executive: false - -jobTypes: - full-time: true - contract: false - part-time: false - temporary: true - internship: false - other: false - volunteer: true - -date: - all time: false - month: true - week: false - 24 hours: true - -positions: - - Software developer - -locations: - - Germany - -distance: 100 - -companyBlacklist: - - Noir - - Crossover - -titleBlacklist: diff --git a/data_folder_example/plain_text_resume.yaml b/data_folder_example/plain_text_resume.yaml deleted file mode 100644 index 1db8cdd..0000000 --- a/data_folder_example/plain_text_resume.yaml +++ /dev/null @@ -1,128 +0,0 @@ -personal_information: - name: "Mario" - surname: "Rossi" - dateOfBirth: "15/09/1988" - country: "Italy" - city: "Milan" - address: "Via Montenapoleone 10, 20121 Milan" - phonePrefix: "+39" - phone: "3351234567" - email: "mario.rossi@techcode.it" - github: "https://github.com/mario-rossi-dev" - linkedin: "https://www.linkedin.com/in/mario-rossi-developer/" - -self_identification: - gender: "Male" - pronouns: "He/Him" - veteran: false - disability: false - ethnicity: "Mediterranean" - -legal_authorization: - euWorkAuthorization: true - usWorkAuthorization: false - requiresUsVisa: true - legallyAllowedToWorkInUs: false - requiresUsSponsorship: true - requiresEuVisa: false - legallyAllowedToWorkInEu: true - requiresEuSponsorship: false - -work_preferences: - remoteWork: true - inPersonWork: true - openToRelocation: true - willingToCompleteAssessments: true - willingToUndergoDrugTests: true - willingToUndergoBackgroundChecks: true - -education_details: - - degree: "Master" - university: "Politecnico di Milano" - gpa: "3.8/4" - graduationYear: "2012" - fieldOfStudy: "Computer Engineering" - skillsAcquired: - artificialIntelligence: "4" - dataScience: "3" - cloudComputing: "3" - -experience_details: - - position: "Senior Software Engineer" - company: "TechInnovate S.p.A." - employmentPeriod: "06/2018 - Present" - location: "Milan, Italy" - industry: "FinTech" - keyResponsibilities: - responsibility1: "Led development of real-time trading algorithm, improving transaction speed by 40%" - responsibility2: "Implemented CI/CD pipeline, reducing deployment time from days to hours" - responsibility3: "Mentored junior developers, increasing team productivity by 25% over 6 months" - skillsAcquired: - java: "5" - springBoot: "4" - kubernetes: "3" - aws: "4" - microservices: "4" - agileMethodologies: "5" - -projects: - project1: "Developed a high-frequency trading platform using Java and Spring Boot, processing over 1 million transactions per second" - project2: "Led the migration of legacy systems to a microservices architecture, improving system reliability by 99.99%" - -availability: - noticePeriod: "3 months" - -salary_expectations: - salaryRangeUSD: "90000" - -certifications: - - "AWS Certified Solutions Architect" - - "Oracle Certified Professional, Java SE 11 Developer" - - "Certified Scrum Master" - -skills: - leadership: 6 - problemSolving: 4 - criticalThinking: 3 - adaptability: 2 - perfectionism: 2 - blockchain: 3 - iot: 3 - python: 3 - fullStackDevelopment: 3 - databaseManagement: 3 - versionControl: 3 - agileMethodologies: 2 - devOpsPractices: 2 - algorithmDesign: 3 - mobileAppDevelopment: 2 - softwareArchitecture: 3 - teamCollaboration: 2 - documentation: 2 - java: 3 - cSharp: 3 - c: 3 - cPlusPlus: 3 - javascript: 3 - php: 3 - sql: 3 - noSql: 3 - mysql: 3 - firebase: 3 - continuousIntegration: 2 - continuousDeployment: 2 - optimization: 3 - -languages: - - language: "Italian" - proficiency: "Native" - - language: "English" - proficiency: "Fluent" - - language: "Spanish" - proficiency: "Intermediate" - -interests: - - "Open Source Contributing" - - "Machine Learning" - - "Hiking" - - "Chess" \ No newline at end of file diff --git a/data_folder_example/secrets.yaml b/data_folder_example/secrets.yaml deleted file mode 100644 index ad24cd8..0000000 --- a/data_folder_example/secrets.yaml +++ /dev/null @@ -1,3 +0,0 @@ -email: myemaillinkedin@gmail.com -password: ImpossiblePassowrd10 -openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR \ No newline at end of file diff --git a/gpt.py b/gpt.py index cbd5a7b..515a82f 100644 --- a/gpt.py +++ b/gpt.py @@ -4,7 +4,7 @@ import re import textwrap from datetime import datetime from typing import Dict, List - +from pathlib import Path from dotenv import load_dotenv from langchain_core.messages.ai import AIMessage from langchain_core.output_parsers import StrOutputParser @@ -25,7 +25,7 @@ class LLMLogger: @staticmethod def log_request(prompts, parsed_reply: Dict[str, Dict]): - calls_log = os.path.join(os.getcwd(), "open_ai_calls.json") + calls_log = os.path.join(Path("data_folder/output"), "open_ai_calls.json") if isinstance(prompts, StringPromptValue): prompts = prompts.text elif isinstance(prompts, Dict): @@ -94,7 +94,6 @@ class LoggerChatModel: response_metadata = llmresult.response_metadata id_ = llmresult.id usage_metadata = llmresult.usage_metadata - parsed_result = { "content": content, "response_metadata": { @@ -116,11 +115,8 @@ class LoggerChatModel: class GPTAnswerer: def __init__(self, openai_api_key): self.llm_cheap = LoggerChatModel( - ChatOpenAI( - model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.8 - ) + ChatOpenAI(model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.8) ) - @property def job_description(self): return self.job.description @@ -148,10 +144,11 @@ class GPTAnswerer: def set_job(self, job): self.job = job - self.job.set_summarize_job_description( - self.summarize_job_description(self.job.description) - ) + self.job.set_summarize_job_description(self.summarize_job_description(self.job.description)) + def set_job_application_profile(self, job_application_profile): + self.job_application_profile = job_application_profile + def summarize_job_description(self, text: str) -> str: strings.summarize_prompt_template = self._preprocess_template_string( strings.summarize_prompt_template @@ -160,40 +157,11 @@ class GPTAnswerer: chain = prompt | self.llm_cheap | StrOutputParser() output = chain.invoke({"text": text}) return output - - - def get_resume_html(self): - resume_markdown_prompt = ChatPromptTemplate.from_template(strings.resume_markdown_template) - fusion_job_description_resume_prompt = ChatPromptTemplate.from_template(strings.fusion_job_description_resume_template) - resume_markdown_chain = resume_markdown_prompt | self.llm_cheap | StrOutputParser() - fusion_job_description_resume_chain = fusion_job_description_resume_prompt | self.llm_cheap | StrOutputParser() - - casual_markdown_path = os.path.abspath("resume_template/casual_markdown.js") - reorganize_header_path = os.path.abspath("resume_template/reorganizeHeader.js") - resume_css_path = os.path.abspath("resume_template/resume.css") - - html_template = strings.html_template.format(casual_markdown=casual_markdown_path, reorganize_header=reorganize_header_path, resume_css=resume_css_path) - composed_chain = ( - resume_markdown_chain - | (lambda output: {"job_description": self.job.summarize_job_description, "formatted_resume": output}) - | fusion_job_description_resume_chain - | (lambda formatted_resume: html_template + formatted_resume) - ) - try: - output = composed_chain.invoke({ - "resume": self.resume, - "job_description": self.job.summarize_job_description - }) - return output - except Exception as e: - #print(f"Error during elaboration: {e}") - pass - - + def _create_chain(self, template: str): prompt = ChatPromptTemplate.from_template(template) return prompt | self.llm_cheap | StrOutputParser() - + def answer_question_textual_wide_range(self, question: str) -> str: # Define chains for each section of the resume chains = { @@ -211,40 +179,118 @@ class GPTAnswerer: "interests": self._create_chain(strings.interests_template), "cover_letter": self._create_chain(strings.coverletter_template), } - section_prompt = ( - f"For the following question: '{question}', which section of the resume is relevant? " - "Respond with one of the following: Personal information, Self Identification, Legal Authorization, " - "Work Preferences, Education Details, Experience Details, Projects, Availability, Salary Expectations, " - "Certifications, Languages, Interests, Cover letter" - ) + section_prompt = """ + You are assisting a bot designed to automatically apply for jobs on LinkedIn. The bot receives various questions about job applications and needs to determine the most relevant section of the resume to provide an accurate response. + + For the following question: '{question}', determine which section of the resume is most relevant. + Respond with exactly one of the following options: + - Personal information + - Self Identification + - Legal Authorization + - Work Preferences + - Education Details + - Experience Details + - Projects + - Availability + - Salary Expectations + - Certifications + - Languages + - Interests + - Cover letter + + Here are detailed guidelines to help you choose the correct section: + + 1. **Personal Information**: + - **Purpose**: Contains your basic contact details and online profiles. + - **Use When**: The question is about how to contact you or requests links to your professional online presence. + - **Examples**: Email address, phone number, LinkedIn profile, GitHub repository, personal website. + + 2. **Self Identification**: + - **Purpose**: Covers personal identifiers and demographic information. + - **Use When**: The question pertains to your gender, pronouns, veteran status, disability status, or ethnicity. + - **Examples**: Gender, pronouns, veteran status, disability status, ethnicity. + + 3. **Legal Authorization**: + - **Purpose**: Details your work authorization status and visa requirements. + - **Use When**: The question asks about your ability to work in specific countries or if you need sponsorship or visas. + - **Examples**: Work authorization in EU and US, visa requirements, legally allowed to work. + + 4. **Work Preferences**: + - **Purpose**: Specifies your preferences regarding work conditions and job roles. + - **Use When**: The question is about your preferences for remote work, in-person work, relocation, and willingness to undergo assessments or background checks. + - **Examples**: Remote work, in-person work, open to relocation, willingness to complete assessments. + + 5. **Education Details**: + - **Purpose**: Contains information about your academic qualifications. + - **Use When**: The question concerns your degrees, universities attended, GPA, and relevant coursework. + - **Examples**: Degree, university, GPA, field of study, exams. + + 6. **Experience Details**: + - **Purpose**: Details your professional work history and key responsibilities. + - **Use When**: The question pertains to your job roles, responsibilities, and achievements in previous positions. + - **Examples**: Job positions, company names, key responsibilities, skills acquired. + + 7. **Projects**: + - **Purpose**: Highlights specific projects you have worked on. + - **Use When**: The question asks about particular projects, their descriptions, or links to project repositories. + - **Examples**: Project names, descriptions, links to project repositories. + + 8. **Availability**: + - **Purpose**: Provides information on your availability for new roles. + - **Use When**: The question is about how soon you can start a new job or your notice period. + - **Examples**: Notice period, availability to start. + + 9. **Salary Expectations**: + - **Purpose**: Covers your expected salary range. + - **Use When**: The question pertains to your salary expectations or compensation requirements. + - **Examples**: Desired salary range. + + 10. **Certifications**: + - **Purpose**: Lists your professional certifications or licenses. + - **Use When**: The question involves your certifications or qualifications from recognized organizations. + - **Examples**: Certification names, issuing bodies, dates of validity. + + 11. **Languages**: + - **Purpose**: Describes the languages you can speak and your proficiency levels. + - **Use When**: The question asks about your language skills or proficiency in specific languages. + - **Examples**: Languages spoken, proficiency levels. + + 12. **Interests**: + - **Purpose**: Details your personal or professional interests. + - **Use When**: The question is about your hobbies, interests, or activities outside of work. + - **Examples**: Personal hobbies, professional interests. + + 13. **Cover Letter**: + - **Purpose**: Contains your personalized cover letter or statement. + - **Use When**: The question involves your cover letter or specific written content intended for the job application. + - **Examples**: Cover letter content, personalized statements. + + Provide only the exact name of the section from the list above with no additional text. + """ + + + prompt = ChatPromptTemplate.from_template(section_prompt) chain = prompt | self.llm_cheap | StrOutputParser() output = chain.invoke({"question": question}) section_name = output.lower().replace(" ", "_") if section_name == "cover_letter": chain = chains.get(section_name) - output= chain.invoke({"resume": self.resume, "job_description": self.job_description}) + output = chain.invoke({"resume": self.resume, "job_description": self.job_description}) return output - resume_section = getattr(self.resume, section_name, None) + resume_section = getattr(self.resume, section_name, None) or getattr(self.job_application_profile, section_name, None) if resume_section is None: - raise ValueError(f"Section '{section_name}' not found in the resume.") + raise ValueError(f"Section '{section_name}' not found in either resume or job_application_profile.") chain = chains.get(section_name) if chain is None: raise ValueError(f"Chain not defined for section '{section_name}'") return chain.invoke({"resume_section": resume_section, "question": question}) - def answer_question_textual(self, question: str) -> str: - template = self._preprocess_template_string(strings.resume_stuff_template) - prompt = ChatPromptTemplate.from_template(template) - chain = prompt | self.llm_cheap | StrOutputParser() - output = chain.invoke({"resume": self.resume, "question": question}) - return output - def answer_question_numeric(self, question: str, default_experience: int = 3) -> int: func_template = self._preprocess_template_string(strings.numeric_question_template) prompt = ChatPromptTemplate.from_template(func_template) chain = prompt | self.llm_cheap | StrOutputParser() - output_str = chain.invoke({"resume": self.resume, "question": question, "default_experience": default_experience}) + output_str = chain.invoke({"resume_educations": self.resume.education_details,"resume_jobs": self.resume.experience_details,"resume_projects": self.resume.projects , "question": question}) try: output = self.extract_number_from_string(output_str) except ValueError: @@ -265,3 +311,20 @@ class GPTAnswerer: output_str = chain.invoke({"resume": self.resume, "question": question, "options": options}) best_option = self.find_best_match(output_str, options) return best_option + + def resume_or_cover(self, phrase: str) -> str: + # Define the prompt template + prompt_template = """ + Given the following phrase, respond with only 'resume' if the phrase is about a resume, or 'cover' if it's about a cover letter. Do not provide any additional information or explanations. + + phrase: {phrase} + """ + prompt = ChatPromptTemplate.from_template(prompt_template) + chain = prompt | self.llm_cheap | StrOutputParser() + response = chain.invoke({"phrase": phrase}) + if "resume" in response: + return "resume" + elif "cover" in response: + return "cover" + else: + return "resume" diff --git a/job.py b/job.py index 301e3fb..9ac1842 100644 --- a/job.py +++ b/job.py @@ -9,6 +9,7 @@ class Job: apply_method: str description: str = "" summarize_job_description: str = "" + pdf_path: str = "" def set_summarize_job_description(self, summarize_job_description): self.summarize_job_description = summarize_job_description diff --git a/job_application_profile.py b/job_application_profile.py new file mode 100644 index 0000000..89bbdb2 --- /dev/null +++ b/job_application_profile.py @@ -0,0 +1,132 @@ +from dataclasses import dataclass +from typing import Dict, List +import yaml + +@dataclass +class SelfIdentification: + gender: str + pronouns: str + veteran: str + disability: str + ethnicity: str + +@dataclass +class LegalAuthorization: + eu_work_authorization: str + us_work_authorization: str + requires_us_visa: str + legally_allowed_to_work_in_us: str + requires_us_sponsorship: str + requires_eu_visa: str + legally_allowed_to_work_in_eu: str + requires_eu_sponsorship: str + +@dataclass +class WorkPreferences: + remote_work: str + in_person_work: str + open_to_relocation: str + willing_to_complete_assessments: str + willing_to_undergo_drug_tests: str + willing_to_undergo_background_checks: str + +@dataclass +class Availability: + notice_period: str + +@dataclass +class SalaryExpectations: + salary_range_usd: str + +@dataclass +class JobApplicationProfile: + self_identification: SelfIdentification + legal_authorization: LegalAuthorization + work_preferences: WorkPreferences + availability: Availability + salary_expectations: SalaryExpectations + + def __init__(self, yaml_str: str): + try: + data = yaml.safe_load(yaml_str) + except yaml.YAMLError as e: + raise ValueError("Error parsing YAML file.") from e + except Exception as e: + raise RuntimeError("An unexpected error occurred while parsing the YAML file.") from e + + if not isinstance(data, dict): + raise TypeError("YAML data must be a dictionary.") + + # Process self_identification + try: + self.self_identification = SelfIdentification(**data['self_identification']) + except KeyError as e: + raise KeyError(f"Required field {e} is missing in self_identification data.") from e + except TypeError as e: + raise TypeError(f"Error in self_identification data: {e}") from e + except AttributeError as e: + raise AttributeError("Attribute error in self_identification processing.") from e + except Exception as e: + raise RuntimeError("An unexpected error occurred while processing self_identification.") from e + + # Process legal_authorization + try: + self.legal_authorization = LegalAuthorization(**data['legal_authorization']) + except KeyError as e: + raise KeyError(f"Required field {e} is missing in legal_authorization data.") from e + except TypeError as e: + raise TypeError(f"Error in legal_authorization data: {e}") from e + except AttributeError as e: + raise AttributeError("Attribute error in legal_authorization processing.") from e + except Exception as e: + raise RuntimeError("An unexpected error occurred while processing legal_authorization.") from e + + # Process work_preferences + try: + self.work_preferences = WorkPreferences(**data['work_preferences']) + except KeyError as e: + raise KeyError(f"Required field {e} is missing in work_preferences data.") from e + except TypeError as e: + raise TypeError(f"Error in work_preferences data: {e}") from e + except AttributeError as e: + raise AttributeError("Attribute error in work_preferences processing.") from e + except Exception as e: + raise RuntimeError("An unexpected error occurred while processing work_preferences.") from e + + # Process availability + try: + self.availability = Availability(**data['availability']) + except KeyError as e: + raise KeyError(f"Required field {e} is missing in availability data.") from e + except TypeError as e: + raise TypeError(f"Error in availability data: {e}") from e + except AttributeError as e: + raise AttributeError("Attribute error in availability processing.") from e + except Exception as e: + raise RuntimeError("An unexpected error occurred while processing availability.") from e + + # Process salary_expectations + try: + self.salary_expectations = SalaryExpectations(**data['salary_expectations']) + except KeyError as e: + raise KeyError(f"Required field {e} is missing in salary_expectations data.") from e + except TypeError as e: + raise TypeError(f"Error in salary_expectations data: {e}") from e + except AttributeError as e: + raise AttributeError("Attribute error in salary_expectations processing.") from e + except Exception as e: + raise RuntimeError("An unexpected error occurred while processing salary_expectations.") from e + + # Process additional fields + + + + def __str__(self): + def format_dataclass(obj): + return "\n".join(f"{field.name}: {getattr(obj, field.name)}" for field in obj.__dataclass_fields__.values()) + + return (f"Self Identification:\n{format_dataclass(self.self_identification)}\n\n" + f"Legal Authorization:\n{format_dataclass(self.legal_authorization)}\n\n" + f"Work Preferences:\n{format_dataclass(self.work_preferences)}\n\n" + f"Availability: {self.availability.notice_period}\n\n" + f"Salary Expectations: {self.salary_expectations.salary_range_usd}\n\n") diff --git a/linkedIn_bot_facade.py b/linkedIn_bot_facade.py index 6e8cd3e..33dc06a 100644 --- a/linkedIn_bot_facade.py +++ b/linkedIn_bot_facade.py @@ -1,57 +1,73 @@ -class LinkedInBotFacade: +class LinkedInBotState: + def __init__(self): + self.reset() + def reset(self): + self.credentials_set = False + self.api_key_set = False + self.job_application_profile_set = False + self.gpt_answerer_set = False + self.parameters_set = False + self.logged_in = False + + def validate_state(self, required_keys): + for key in required_keys: + if not getattr(self, key): + raise ValueError(f"{key.replace('_', ' ').capitalize()} must be set before proceeding.") + +class LinkedInBotFacade: def __init__(self, login_component, apply_component): self.login_component = login_component self.apply_component = apply_component - self.state = { - "credentials_set": False, - "api_key_set": False, - "resume_set": False, - "gpt_answerer_set": False, - "parameters_set": False, - "logged_in": False - } + self.state = LinkedInBotState() + self.job_application_profile = None + self.resume = None + self.email = None + self.password = None + self.parameters = None - def set_resume(self, resume): - if not resume: - raise ValueError("Plain text resume cannot be empty.") + def set_job_application_profile_and_resume(self, job_application_profile, resume): + self._validate_non_empty(job_application_profile, "Job application profile") + self._validate_non_empty(resume, "Resume") + self.job_application_profile = job_application_profile self.resume = resume - self.state["resume_set"] = True + self.state.job_application_profile_set = True - def set_secrets(self, email, password): # Aggiunto openai_api_key - if not email or not password : - raise ValueError("Email and password cannot be empty.") + def set_secrets(self, email, password): + self._validate_non_empty(email, "Email") + self._validate_non_empty(password, "Password") self.email = email self.password = password - self.state["credentials_set"] = True + self.state.credentials_set = True - def set_gpt_answerer(self, gpt_answerer_component): - self.gpt_answerer = gpt_answerer_component - self.gpt_answerer.set_resume(self.resume) - self.apply_component.set_gpt_answerer(self.gpt_answerer) - self.state["gpt_answerer_set"] = True + def set_gpt_answerer_and_resume_generator(self, gpt_answerer_component, resume_generator_manager): + self._ensure_job_profile_and_resume_set() + gpt_answerer_component.set_job_application_profile(self.job_application_profile) + gpt_answerer_component.set_resume(self.resume) + self.apply_component.set_gpt_answerer(gpt_answerer_component) + self.apply_component.set_resume_generator_manager(resume_generator_manager) + self.state.gpt_answerer_set = True def set_parameters(self, parameters): - if not parameters: - raise ValueError("Parameters cannot be None or empty.") + self._validate_non_empty(parameters, "Parameters") self.parameters = parameters self.apply_component.set_parameters(parameters) - self.state["parameters_set"] = True + self.state.parameters_set = True def start_login(self): - if not self.state["credentials_set"]: - raise ValueError("Email and password must be set before logging in.") + self.state.validate_state(['credentials_set']) self.login_component.set_secrets(self.email, self.password) self.login_component.start() - self.state["logged_in"] = True + self.state.logged_in = True def start_apply(self): - if not self.state["logged_in"]: - raise ValueError("You must be logged in before applying.") - if not self.state["resume_set"]: - raise ValueError("Plain text resume must be set before applying.") - if not self.state["gpt_answerer_set"]: - raise ValueError("GPT Answerer must be set before applying.") - if not self.state["parameters_set"]: - raise ValueError("Parameters must be set before applying.") - self.apply_component.start_applying() \ No newline at end of file + self.state.validate_state(['logged_in', 'job_application_profile_set', 'gpt_answerer_set', 'parameters_set']) + self.apply_component.start_applying() + + def _validate_non_empty(self, value, name): + if not value: + raise ValueError(f"{name} cannot be empty.") + + def _ensure_job_profile_and_resume_set(self): + if not self.state.job_application_profile_set: + raise ValueError("Job application profile and resume must be set before proceeding.") diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index 4bc0451..bf931b3 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -1,12 +1,12 @@ import base64 import os import random +import re import tempfile import time import traceback from datetime import date from typing import List, Optional, Any, Tuple -import uuid from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas from selenium.common.exceptions import NoSuchElementException @@ -15,27 +15,18 @@ from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import Select, WebDriverWait -import tempfile -import time -from reportlab.lib.pagesizes import letter -from reportlab.pdfgen import canvas -import io -import time -from reportlab.lib.pagesizes import letter -from reportlab.pdfgen import canvas -from reportlab.lib.pagesizes import letter -from xhtml2pdf import pisa - -import utils +from selenium.webdriver import ActionChains +import utils class LinkedInEasyApplier: - def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any): + def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any, resume_generator_manager): if resume_dir is None or not os.path.exists(resume_dir): resume_dir = None self.driver = driver self.resume_dir = resume_dir self.set_old_answers = set_old_answers self.gpt_answerer = gpt_answerer + self.resume_generator_manager = resume_generator_manager def job_apply(self, job: Any): self.driver.get(job.link) @@ -44,55 +35,64 @@ class LinkedInEasyApplier: easy_apply_button = self._find_easy_apply_button() job_description = self._get_job_description() job.set_job_description(job_description) - easy_apply_button.click() + actions = ActionChains(self.driver) + actions.move_to_element(easy_apply_button).click().perform() self.gpt_answerer.set_job(job) - self._fill_application_form() + self._fill_application_form(job) except Exception: tb_str = traceback.format_exc() self._discard_application() raise Exception(f"Failed to apply to job! Original exception: \nTraceback:\n{tb_str}") - def _find_easy_apply_button(self) -> WebElement: - buttons = WebDriverWait(self.driver, 10).until( - EC.presence_of_all_elements_located( - (By.XPATH, '//button[contains(@class, "jobs-apply-button") and contains(., "Easy Apply")]') - ) - ) - for index, button in enumerate(buttons): - try: - return WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable( - (By.XPATH, f'(//button[contains(@class, "jobs-apply-button") and contains(., "Easy Apply")])[{index + 1}]') - ) + attempt = 0 + while attempt < 2: + self._scroll_page() + buttons = WebDriverWait(self.driver, 10).until( + EC.presence_of_all_elements_located( + (By.XPATH, '//button[contains(@class, "jobs-apply-button") and contains(., "Easy Apply")]') ) - except Exception as e: - pass + ) + for index, _ in enumerate(buttons): + try: + button = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable( + (By.XPATH, f'(//button[contains(@class, "jobs-apply-button") and contains(., "Easy Apply")])[{index + 1}]') + ) + ) + return button + except Exception as e: + pass + if attempt == 0: + self.driver.refresh() + time.sleep(3) + attempt += 1 raise Exception("No clickable 'Easy Apply' button found") + def _get_job_description(self) -> str: try: see_more_button = self.driver.find_element(By.XPATH, '//button[@aria-label="Click to see more description"]') - see_more_button.click() + actions = ActionChains(self.driver) + actions.move_to_element(see_more_button).click().perform() time.sleep(2) description = self.driver.find_element(By.CLASS_NAME, 'jobs-description-content__text').text - self._scroll_page() return description except NoSuchElementException: tb_str = traceback.format_exc() raise Exception("Job description 'See more' button not found: \nTraceback:\n{tb_str}") - except Exception : + except Exception: tb_str = traceback.format_exc() raise Exception(f"Error getting Job description: \nTraceback:\n{tb_str}") def _scroll_page(self) -> None: scrollable_element = self.driver.find_element(By.TAG_NAME, 'html') - utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=False) - utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=True) + #utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=False) + #utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=True) - def _fill_application_form(self): + def _fill_application_form(self, job): while True: - self.fill_up() + self.fill_up(job) if self._next_or_submit(): break @@ -110,7 +110,6 @@ class LinkedInEasyApplier: time.sleep(random.uniform(3.0, 5.0)) self._check_for_errors() - def _unfollow_company(self) -> None: try: follow_checkbox = self.driver.find_element( @@ -133,88 +132,57 @@ class LinkedInEasyApplier: except Exception as e: pass - def fill_up(self) -> None: - try: - easy_apply_content = self.driver.find_element(By.CLASS_NAME, 'jobs-easy-apply-content') - pb4_elements = easy_apply_content.find_elements(By.CLASS_NAME, 'pb4') - for element in pb4_elements: - self._process_form_element(element) - except Exception as e: - pass + def fill_up(self, job) -> None: + easy_apply_content = self.driver.find_element(By.CLASS_NAME, 'jobs-easy-apply-content') + pb4_elements = easy_apply_content.find_elements(By.CLASS_NAME, 'pb4') + for element in pb4_elements: + self._process_form_element(element, job) - - - def _process_form_element(self, element: WebElement) -> None: - try: - if self._is_upload_field(element): - self._handle_upload_fields(element) - else: - self._fill_additional_questions() - except Exception as e: - pass + def _process_form_element(self, element: WebElement, job) -> None: + if self._is_upload_field(element): + self._handle_upload_fields(element, job) + else: + self._fill_additional_questions() def _is_upload_field(self, element: WebElement) -> bool: - try: - element.find_element(By.XPATH, ".//input[@type='file']") - return True - except NoSuchElementException: - return False + return bool(element.find_elements(By.XPATH, ".//input[@type='file']")) - def _handle_upload_fields(self, element: WebElement) -> None: + def _handle_upload_fields(self, element: WebElement, job) -> None: file_upload_elements = self.driver.find_elements(By.XPATH, "//input[@type='file']") for element in file_upload_elements: parent = element.find_element(By.XPATH, "..") self.driver.execute_script("arguments[0].classList.remove('hidden')", element) - if 'resume' in parent.text.lower(): - if self.resume_dir != None: + output = self.gpt_answerer.resume_or_cover(parent.text.lower()) + if 'resume' in output: + if self.resume_dir: resume_path = self.resume_dir.resolve() - if self.resume_dir != None and resume_path.exists() and resume_path.is_file(): - element.send_keys(str(resume_path)) - else: - self._create_and_upload_resume(element) - elif 'cover' in parent.text.lower(): + if resume_path.exists() and resume_path.is_file(): + element.send_keys(str(resume_path)) + else: + self._create_and_upload_resume(element, job) + elif 'cover' in output: self._create_and_upload_cover_letter(element) - def _create_and_upload_resume(self, element): - max_retries = 3 - retry_delay = 1 + def _create_and_upload_resume(self, element, job): folder_path = 'generated_cv' - - if not os.path.exists(folder_path): - os.makedirs(folder_path) - for attempt in range(max_retries): - try: - html_string = self.gpt_answerer.get_resume_html() - with tempfile.NamedTemporaryFile(delete=False, suffix='.html', mode='w', encoding='utf-8') as temp_html_file: - temp_html_file.write(html_string) - file_name_HTML = temp_html_file.name - - file_name_pdf = f"resume_{uuid.uuid4().hex}.pdf" - file_path_pdf = os.path.join(folder_path, file_name_pdf) - - with open(file_path_pdf, "wb") as f: - f.write(base64.b64decode(utils.HTML_to_PDF(file_name_HTML))) - - element.send_keys(os.path.abspath(file_path_pdf)) - time.sleep(2) # Give some time for the upload process - os.remove(file_name_HTML) - return True - except Exception: - if attempt < max_retries - 1: - time.sleep(retry_delay) - else: - tb_str = traceback.format_exc() - raise Exception(f"Max retries reached. Upload failed: \nTraceback:\n{tb_str}") - - def _upload_resume(self, element: WebElement) -> None: - element.send_keys(str(self.resume_dir)) + os.makedirs(folder_path, exist_ok=True) + try: + file_path_pdf = os.path.join(folder_path, f"CV_{random.randint(0, 9999)}.pdf") + with open(file_path_pdf, "xb") as f: + f.write(base64.b64decode(self.resume_generator_manager.pdf_base64(job_description_text=job.description))) + element.send_keys(os.path.abspath(file_path_pdf)) + job.pdf_path = os.path.abspath(file_path_pdf) + time.sleep(2) + except Exception: + tb_str = traceback.format_exc() + raise Exception(f"Upload failed: \nTraceback:\n{tb_str}") def _create_and_upload_cover_letter(self, element: WebElement) -> None: cover_letter = self.gpt_answerer.answer_question_textual_wide_range("Write a cover letter") with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf_file: letter_path = temp_pdf_file.name c = canvas.Canvas(letter_path, pagesize=letter) - width, height = letter + _, height = letter text_object = c.beginText(100, height - 100) text_object.setFont("Helvetica", 12) text_object.textLines(cover_letter) @@ -225,131 +193,84 @@ class LinkedInEasyApplier: def _fill_additional_questions(self) -> None: form_sections = self.driver.find_elements(By.CLASS_NAME, 'jobs-easy-apply-form-section__grouping') for section in form_sections: - self._process_question(section) + self._process_form_section(section) - def _process_question(self, section: WebElement) -> None: + def _process_form_section(self, section: WebElement) -> None: if self._handle_terms_of_service(section): return - self._handle_radio_question(section) - self._handle_textbox_question(section) - self._handle_date_question(section) - self._handle_dropdown_question(section) + if self._find_and_handle_radio_question(section): + return + if self._find_and_handle_textbox_question(section): + return + if self._find_and_handle_date_question(section): + return + if self._find_and_handle_dropdown_question(section): + return def _handle_terms_of_service(self, element: WebElement) -> bool: - try: - question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element') - checkbox = question.find_element(By.TAG_NAME, 'label') - question_text = question.text.lower() - if 'terms of service' in question_text or 'privacy policy' in question_text or 'terms of use' in question_text: - checkbox.click() - return True - except NoSuchElementException: - pass + checkbox = element.find_elements(By.TAG_NAME, 'label') + if checkbox and any(term in checkbox[0].text.lower() for term in ['terms of service', 'privacy policy', 'terms of use']): + checkbox[0].click() + return True return False - def _handle_radio_question(self, element: WebElement) -> None: - try: - question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element') - radios = question.find_elements(By.CLASS_NAME, 'fb-text-selectable__option') - if not radios: - return - - question_text = element.text.lower() + def _find_and_handle_radio_question(self, section: WebElement) -> bool: + radios = section.find_elements(By.CLASS_NAME, 'fb-text-selectable__option') + if radios: + question_text = section.text.lower() options = [radio.text.lower() for radio in radios] - - answer = self._get_answer_from_set('radio', question_text, options) - if not answer: - answer = self.gpt_answerer.answer_question_from_options(question_text, options) - + answer = self.gpt_answerer.answer_question_from_options(question_text, options) self._select_radio(radios, answer) - except Exception: - pass - - def _handle_textbox_question(self, element: WebElement) -> None: - try: - question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element') - question_text = question.find_element(By.TAG_NAME, 'label').text.lower() - text_field = self._find_text_field(question) - - is_numeric = self._is_numeric_field(text_field) - answer = self._get_answer_from_set('numeric' if is_numeric else 'text', question_text) - - if not answer: - answer = self.gpt_answerer.answer_question_numeric(question_text) if is_numeric else self.gpt_answerer.answer_question_textual_wide_range(question_text) - - self._enter_text(text_field, answer) - self._handle_form_errors(element, question_text, answer, text_field) - except Exception: - pass - - def _handle_date_question(self, element: WebElement) -> None: - try: - date_picker = element.find_element(By.CLASS_NAME, 'artdeco-datepicker__input') - date_picker.clear() - date_picker.send_keys(date.today().strftime("%m/%d/%y")) - time.sleep(3) - date_picker.send_keys(Keys.RETURN) - time.sleep(2) - except Exception: - pass - - def _handle_dropdown_question(self, element: WebElement) -> None: - try: - question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element') - question_text = question.find_element(By.TAG_NAME, 'label').text.lower() - dropdown = question.find_element(By.TAG_NAME, 'select') - select = Select(dropdown) - options = [option.text for option in select.options] - - answer = self._get_answer_from_set('dropdown', question_text, options) - if not answer: - answer = self.gpt_answerer.answer_question_from_options(question_text, options) - - self._select_dropdown(dropdown, answer) - except Exception: - pass - - def _get_answer_from_set(self, question_type: str, question_text: str, options: Optional[List[str]] = None) -> Optional[str]: - for entry in self.set_old_answers: - if isinstance(entry, tuple) and len(entry) == 3: - if entry[0] == question_type and question_text in entry[1].lower(): - answer = entry[2] - return answer if options is None or answer in options else None - return None - - def _find_text_field(self, question: WebElement) -> WebElement: - try: - return question.find_element(By.TAG_NAME, 'input') - except NoSuchElementException: - return question.find_element(By.TAG_NAME, 'textarea') - - def _is_numeric_field(self, field: WebElement) -> bool: - field_type = field.get_attribute('type').lower() - if 'numeric' in field_type: return True - class_attribute = field.get_attribute("id") - return class_attribute and 'numeric' in class_attribute + return False + + def _find_and_handle_textbox_question(self, section: WebElement) -> bool: + text_fields = section.find_elements(By.TAG_NAME, 'input') + section.find_elements(By.TAG_NAME, 'textarea') + if text_fields: + text_field = text_fields[0] + question_text = section.find_element(By.TAG_NAME, 'label').text.lower() + is_numeric = self._is_numeric_field(text_field) + answer = self.gpt_answerer.answer_question_numeric(question_text) if is_numeric else self.gpt_answerer.answer_question_textual_wide_range(question_text) + self._enter_text(text_field, answer) + return True + return False + + def _find_and_handle_date_question(self, section: WebElement) -> bool: + date_fields = section.find_elements(By.CLASS_NAME, 'artdeco-datepicker__input ') + if date_fields: + date_field = date_fields[0] + answer_date = self.gpt_answerer.answer_question_date() + self._enter_text(date_field, answer_date.strftime("%Y-%m-%d")) + return True + return False + + def _find_and_handle_dropdown_question(self, section: WebElement) -> bool: + dropdowns = section.find_elements(By.CLASS_NAME, 'fb-dropdown__select') + if dropdowns: + dropdown = dropdowns[0] + question_text = section.text.lower() + select = Select(dropdown) + answer = self.gpt_answerer.answer_question_from_options(question_text, [option.text.lower() for option in select.options]) + self._select_dropdown_option(select, answer) + return True + return False + + def _is_numeric_field(self, element: WebElement) -> bool: + input_type = element.get_attribute('type') + return input_type == 'number' def _enter_text(self, element: WebElement, text: str) -> None: element.clear() element.send_keys(text) - def _select_dropdown(self, element: WebElement, text: str) -> None: - select = Select(element) - select.select_by_visible_text(text) - def _select_radio(self, radios: List[WebElement], answer: str) -> None: for radio in radios: - if answer in radio.text.lower(): - radio.find_element(By.TAG_NAME, 'label').click() - return - radios[-1].find_element(By.TAG_NAME, 'label').click() + if radio.text.lower() == answer.lower(): + radio.click() + break - def _handle_form_errors(self, element: WebElement, question_text: str, answer: str, text_field: WebElement) -> None: - try: - error = element.find_element(By.CLASS_NAME, 'artdeco-inline-feedback--error') - error_text = error.text.lower() - new_answer = self.gpt_answerer.try_fix_answer(question_text, answer, error_text) - self._enter_text(text_field, new_answer) - except NoSuchElementException: - pass + def _select_dropdown_option(self, select: Select, answer: str) -> None: + for option in select.options: + if option.text.lower() == answer.lower(): + select.select_by_visible_text(option.text) + break diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index 6fbe3bf..af17877 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -1,4 +1,3 @@ -import csv import os import random import time @@ -10,6 +9,7 @@ from selenium.webdriver.common.by import By import utils from job import Job from linkedIn_easy_applier import LinkedInEasyApplier +import json class EnvironmentKeys: @@ -45,15 +45,15 @@ class LinkedInJobManager: self.resume_dir = None self.output_file_directory = Path(parameters['outputFileDirectory']) self.env_config = EnvironmentKeys() - self.old_question() + #self.old_question() def set_gpt_answerer(self, gpt_answerer): self.gpt_answerer = gpt_answerer - def old_question(self): - """ - Load old answers from a CSV file into a dictionary. - """ + def set_resume_generator_manager(self, resume_generator_manager): + self.resume_generator_manager = resume_generator_manager + + """ def old_question(self): self.set_old_answers = {} file_path = 'data_folder/output/old_Questions.csv' if os.path.exists(file_path): @@ -62,13 +62,11 @@ class LinkedInJobManager: for row in csv_reader: if len(row) == 3: answer_type, question_text, answer = row - self.set_old_answers[(answer_type.lower(), question_text.lower())] = answer + self.set_old_answers[(answer_type.lower(), question_text.lower())] = answer""" def start_applying(self): - self.easy_applier_component = LinkedInEasyApplier( - self.driver, self.resume_dir, self.set_old_answers, self.gpt_answerer - ) + self.easy_applier_component = LinkedInEasyApplier(self.driver, self.resume_dir, self.set_old_answers, self.gpt_answerer, self.resume_generator_manager) searches = list(product(self.positions, self.locations)) random.shuffle(searches) page_sleep = 0 @@ -127,50 +125,50 @@ class LinkedInJobManager: job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list") utils.scroll_slow(self.driver, job_results) utils.scroll_slow(self.driver, job_results, step=300, reverse=True) - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') - if not job_list_elements: raise Exception("No job class elements found on page") - - job_list = [Job(*self.extract_job_information_from_tile(job_element)) for job_element in job_list_elements] - + job_list = [Job(*self.extract_job_information_from_tile(job_element)) for job_element in job_list_elements] for job in job_list: if self.is_blacklisted(job.title, job.company, job.link): utils.printyellow(f"Blacklisted {job.title} at {job.company}, skipping...") - self.write_to_file(job.company, job.location, job.title, job.link, "skipped") + self.write_to_file(job, "skipped") continue - try: if job.apply_method not in {"Continue", "Applied", "Apply"}: self.easy_applier_component.job_apply(job) + self.write_to_file(job, "success") except Exception as e: utils.printred(traceback.format_exc()) - self.write_to_file(job.company, job.location, job.title, job.link, "failed") - continue - self.write_to_file(job.company, job.location, job.title, job.link, "success") + self.write_to_file(job, "failed") + continue except Exception as e: traceback.format_exc() raise e - def write_to_file(self, company, job_title, link, job_location, file_name): - to_write = [company, job_title, link, job_location] - file_path = self.output_file_directory / f"{file_name}.csv" - with open(file_path, 'a', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - writer.writerow(to_write) - - def record_gpt_answer(self, answer_type, question_text, gpt_response): - to_write = [answer_type, question_text, gpt_response] - file_path = self.output_file_directory / "registered_jobs.csv" - try: - with open(file_path, 'a', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - writer.writerow(to_write) - except Exception as e: - utils.printred(f"Error writing registered job: {e}") - utils.printred(f"Details: Answer type: {answer_type}, Question: {question_text}") + def write_to_file(self, job, file_name): + data = { + "company": job.company, + "job_title": job.title, + "link": job.link, + "job_location": job.location, + "pdf_path": job.pdf_path + } + file_path = self.output_file_directory / f"{file_name}.json" + if not file_path.exists(): + with open(file_path, 'w', encoding='utf-8') as f: + json.dump([data], f, indent=4) + else: + with open(file_path, 'r+', encoding='utf-8') as f: + try: + existing_data = json.load(f) + except json.JSONDecodeError: + existing_data = [] + existing_data.append(data) + f.seek(0) + json.dump(existing_data, f, indent=4) + f.truncate() def get_base_search_url(self, parameters): url_parts = [] @@ -205,12 +203,6 @@ class LinkedInJobManager: company = job_tile.find_element(By.CLASS_NAME, 'job-card-container__primary-description').text except: pass - try: - hiring_line = job_tile.find_element(By.XPATH, '//span[contains(.,\' is hiring for this\')]') - hiring_line_text = hiring_line.text - name_terminating_index = hiring_line_text.find(' is hiring for this') - except: - pass try: job_location = job_tile.find_element(By.CLASS_NAME, 'job-card-container__metadata-item').text except: diff --git a/main.py b/main.py index dff3e9b..065fc52 100644 --- a/main.py +++ b/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)}") diff --git a/requirements.txt b/requirements.txt index ca668541d412121243189e06d755beeb7f37c727..b7ba6dc32928a82aa24cba0d1836b795225a47c4 100644 GIT binary patch literal 834 zcmd6l-A=+l6olv6#3wPdP&5P+6TQ>KcR*UOG!{y_Ao%j?cTSNvA>}k7Zf37}FVR=%{D-@9rdkuxj0Y-MA;~ z*QsNxGSsNOxAZx4?>Y(nw)~MJj-4>fno2cF*8kM8hWhaxz?4jTfi2!GnIXol(vI6y zGpZEsy{02efAgt3s9R3+=H@>0er0;|)VX)$$`fyJ6m+YY(VQ>PO;Kz9hFGuY&6uW> v@r}J!stM~kTyr^Rm$E9jYp@eN=t+ => <TAG*gt; - md.formatTag = function (html) { return html.replace(//g,'>'); } - - // frontmatter for simple YAML (support multi-level, but string value only) - md.formatYAML = function (front, matter) { - var level = {}, latest = md.yaml; - matter.replace( /^\s*#(.*)$/gm, '' ).replace( /^( *)([^:^\n]+):(.*)$/gm, function(m, sp, key,val) { - level[sp] = level[sp] || latest - latest = level[sp][key.trim()] = val.trim() || {} - for (e in level) if(e>sp) level[e]=null; - } ); - return '' - } - - //===== format code-block, highlight remarks/keywords for code/sql - md.formatCode = function (match, title, block) { - // convert tag <> to < > tab to 3 space, support marker using ^^^ - block = block.replace(//g,'>') - block = block.replace(/\t/g,' ').replace(/\^\^\^(.+?)\^\^\^/g, '$1') - - // highlight comment and keyword based on title := none | sql | code - if (title.toLowerCase(title) == 'sql') { - block = block.replace(/^\-\-(.*)/gm,'--$1').replace(/\s\-\-(.*)/gm,' --$1') - block = block.replace(/(\s?)(function|procedure|return|if|then|else|end|loop|while|or|and|case|when)(\s)/gim,'$1$2$3') - block = block.replace(/(\s?)(select|update|delete|insert|create|from|where|group by|having|set)(\s)/gim,'$1$2$3') - } else if ((title||'none')!=='none') { - block = block.replace(/^\/\/(.*)/gm,'//$1').replace(/\s\/\/(.*)/gm,' //$1') - block = block.replace(/(\s?)(function|procedure|return|exit|if|then|else|end|loop|while|or|and|case|when)(\s)/gim,'$1$2$3') - block = block.replace(/(\s?)(var|let|const|=>|for|next|do|while|loop|continue|break|switch|try|catch|finally)(\s)/gim,'$1$2$3') - } - - return '
'  + block + '
' - } - - // copy to clipboard for code-block - md.clipboard = function (e) { - navigator.clipboard.writeText( e.parentNode.innerText.replace('copy\n','') ) - e.innerText = 'copied' - } - - //===== parse markdown string into HTML string (exclude code-block) - md.parser = function( mdstr ) { - - // apply yaml variables - for (var name in this.yaml) mdstr = mdstr.replace( new RegExp('\{\{\\s*'+name+'\\s*\}\}', 'gm'), this.yaml[name] ) - - // table syntax - mdstr = mdstr.replace(/\n(.+?)\n.*?\-\-\s?\|\s?\-\-.*?\n([\s\S]*?)\n\s*?\n/g, function (m,p1,p2) { - var thead = p1.replace(/^\|(.+)/gm,'$1').replace(/(.+)\|$/gm,'$1').replace(/\|/g,'') - var tbody = p2.replace(/^\|(.+)/gm,'$1').replace(/(.+)\|$/gm,'$1') - tbody = tbody.replace(/(.+)/gm,'$1').replace(/\|/g,'') - return '\n\n\n\n' + tbody + '\n
' + thead + '\n
\n\n' - } ) - - // horizontal rule =>
- mdstr = mdstr.replace(/^-{3,}|^\_{3,}|^\*{3,}$/gm, '
').replace(/\n\n/g, '\n

') - - // header =>

..

- mdstr = mdstr.replace(/^##### (.*?)\s*#*$/gm, '
$1
') - .replace(/^#### (.*?)\s*#*$/gm, '

$1

') - .replace(/^### (.*?)\s*#*$/gm, '

$1

') - .replace(/^## (.*?)\s*#*$/gm, '

$1

') - .replace(/^# (.*?)\s*#*$/gm, '

$1

') - .replace(/^(.*?)\s*{(.*)}\s*<\/h\d\>$/gm, '$2') - - // inline code-block: `code-block` => code-block - mdstr = mdstr.replace(/``(.*?)``/gm, function(m,p){ return '' + md.formatTag(p).replace(/`/g,'`') + ''} ) - mdstr = mdstr.replace(/`(.*?)`/gm, '$1' ) - - // blockquote, max 2 levels =>
{text}
- mdstr = mdstr.replace(/^\>\> (.*$)/gm, '
$1
') - mdstr = mdstr.replace(/^\> (.*$)/gm, '
$1
') - mdstr = mdstr.replace(/<\/blockquote\>\n/g, '\n
' ) - mdstr = mdstr.replace(/<\/blockquote\>\n/g, '\n
' ) - - // image syntax: ![title](url) => title - mdstr = mdstr.replace(/!\[(.*?)\]\((.*?) "(.*?)"\)/gm, '$1') - mdstr = mdstr.replace(/!\[(.*?)\]\((.*?)\)/gm, '$1') - - // links syntax: [title "title"](url) => text - mdstr = mdstr.replace(/\[(.*?)\]\((.*?) "new"\)/gm, '$1') - mdstr = mdstr.replace(/\[(.*?)\]\((.*?) "(.*?)"\)/gm, '$1') - mdstr = mdstr.replace(/([<\s])(https?\:\/\/.*?)([\s\>])/gm, '$1$2$3') - mdstr = mdstr.replace(/\[(.*?)\]\(\)/gm, '$1') - mdstr = mdstr.replace(/\[(.*?)\]\((.*?)\)/gm, '$1') - - // unordered/ordered list, max 2 levels =>
  • ..
,
  1. ..
- mdstr = mdstr.replace(/^[\*+-][ .](.*)/gm, '
  • $1
' ) - mdstr = mdstr.replace(/^\d\d?[ .](.*)/gm, '
  1. $1
' ) - mdstr = mdstr.replace(/^\s{2,6}[\*+-][ .](.*)/gm, '
    • $1
' ) - mdstr = mdstr.replace(/^\s{2,6}\d[ .](.*)/gm, '
    1. $1
' ) - mdstr = mdstr.replace(/<\/[ou]l\>\n\n?<[ou]l\>/g, '\n' ) - mdstr = mdstr.replace(/<\/[ou]l\>\n<[ou]l\>/g, '\n' ) - - // text decoration: bold, italic, underline, strikethrough, highlight - mdstr = mdstr.replace(/\*\*\*(\w.*?[^\\])\*\*\*/gm, '$1') - mdstr = mdstr.replace(/\*\*(\w.*?[^\\])\*\*/gm, '$1') - mdstr = mdstr.replace(/\*(\w.*?[^\\])\*/gm, '$1') - mdstr = mdstr.replace(/___(\w.*?[^\\])___/gm, '$1') - mdstr = mdstr.replace(/__(\w.*?[^\\])__/gm, '$1') - // mdstr = mdstr.replace(/_(\w.*?[^\\])_/gm, '$1') // NOT support!! - mdstr = mdstr.replace(/\^\^\^(.+?)\^\^\^/gm, '$1') - mdstr = mdstr.replace(/\^\^(\w.*?)\^\^/gm, '$1') - mdstr = mdstr.replace(/~~(\w.*?)~~/gm, '$1') - - // line break and paragraph =>

- mdstr = mdstr.replace(/ \n/g, '\n
').replace(/\n\s*\n/g, '\n

\n') - - // indent as code-block - mdstr = mdstr.replace(/^ {4,10}(.*)/gm, function(m,p) { return '

' + md.formatTag(p) + '
'} ) - mdstr = mdstr.replace(/^\t(.*)/gm, function(m,p) { return '
' + md.formatTag(p) + '
'} ) - mdstr = mdstr.replace(/<\/code\><\/pre\>\n/g, '\n' ) - - // Escaping Characters - return mdstr.replace(/\\([`_~\*\+\-\.\^\\\<\>\(\)\[\]])/gm, '$1' ) - } - - //===== parse markdown string into HTML content (cater code-block) - md.html = function (mdText) { - // replace \r\n to \n, and handle front matter for simple YAML - mdText = mdText.replace(/\r\n/g, '\n').replace( /^---+\s*\n([\s\S]*?)\n---+\s*\n/, md.formatYAML ) - // handle code-block. - mdText = mdText.replace(/\n~~~/g,'\n```').replace(/\n``` *(.*?)\n([\s\S]*?)\n``` *\n/g, md.formatCode) - - // split by "", skip for code-block and process normal text - var pos1=0, pos2=0, mdHTML = '' - while ( (pos1 = mdText.indexOf('')) >= 0 ) { - pos2 = mdText.indexOf('', pos1 ) - mdHTML += md.after( md.parser( md.before( mdText.substr(0,pos1) ) ) ) - mdHTML += mdText.substr(pos1, (pos2>0? pos2-pos1+7 : mdtext.length) ) - mdText = mdText.substr( pos2 + 7 ) - } - - return '
' + mdHTML + md.after( md.parser( md.before(mdText) ) ) + '
' - } - - //===== TOC support - md.toc = function (srcDiv, tocDiv, options ) { - - // select elements, set title - var tocSelector = (options&&options.css) || 'h1,h2,h3,h4' - var tocTitle = (options&&options.title) || 'Table of Contents' - var toc = document.getElementById(srcDiv).querySelectorAll( tocSelector ) - var html = '
    ' + (tocTitle=='none'? '' : '

    ' + tocTitle + '

    '); - - // loop for each element,add
  • element with class in TAG name. - for (var i=0; i' - html += toc[i].textContent + '
  • '; - } - - document.getElementById(tocDiv).innerHTML = html + "
"; - - //===== scrollspy support (ps: add to document.body if element(scrollspy) not found) - if ( options && options.scrollspy ) { - - (document.getElementById(options.scrollspy)||document).onscroll = function () { - - // get TOC elements, and viewport position - var list = document.getElementById(tocDiv).querySelectorAll('li') - var divScroll = document.getElementById(options.scrollspy) || document.documentElement - var divHeight = divScroll.clientHeight || divScroll.offsetHeight - - // loop for each TOC element, add/remove scrollspy class - for (var i=0; i0 && pos { - headerInfoDiv.appendChild(el); - }); - const contactInfoDiv = document.createElement('div'); - contactInfoDiv.className = 'contact-info'; - contactInfoElements.forEach(el => { - contactInfoDiv.appendChild(el); - }); - newHeader.appendChild(headerInfoDiv); - newHeader.appendChild(contactInfoDiv); - const h2 = document.querySelector('h2'); - if (h2) { - h2.parentNode.insertBefore(newHeader, h2); - } -} - - setTimeout(function() { - reorganizeHeader(); - }, 1000); diff --git a/resume_template/resume.css b/resume_template/resume.css deleted file mode 100644 index aff67eb..0000000 --- a/resume_template/resume.css +++ /dev/null @@ -1,156 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Roboto:wght@400&display=swap'); - -/* Reset generale per uniformità */ -body, h2, h3, h4, p, ul, ol { - margin: 0; - padding: 0; - font-family: 'Roboto', sans-serif; -} - -body { - line-height: 1.6; - margin: auto; - padding: 20px; - max-width: 1024px; - color: #333; - background-color: #f8f9fa; -} - -/* Header Style */ -header { - background-color: #e9ecef; /* Sfondo leggermente più scuro */ - padding: 15px 20px 0 20px; /* Padding: 15px sopra, 20px a destra e a sinistra, 0px sotto */ - border-bottom: 2px solid #d1d1d1; /* Bordo sottile per separare il header dal resto del contenuto */ - font-family: 'Roboto', sans-serif; /* Font per il testo generico */ - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-info { - flex: 1; -} - -.header-info h1 { - margin: 0; - font-size: 30px; /* Dimensione del font aumentata per il nome */ - color: #000; /* Colore nero per il testo del nome */ - font-family: 'Montserrat', sans-serif; /* Font più accattivante per il nome */ -} - -.header-info a { - color: #0056b3; /* Colore blu intenso per i link */ - text-decoration: none; - font-weight: bold; -} - -.header-info a:hover { - text-decoration: underline; -} - -.contact-info { - flex: 0; - text-align: center; /* Centratura del testo */ -} - -.contact-info a { - display: block; - color: #0056b3; /* Colore blu intenso per i link */ - text-decoration: none; - margin-bottom: 5px; /* Spaziatura tra i contatti */ - font-size: 14px; /* Dimensione del font per i dettagli di contatto */ - font-family: 'Roboto', sans-serif; /* Font moderno per i dettagli di contatto */ - font-weight: bold; -} - -.contact-info a:hover { - color: #ff5722; /* Colore arancione per i link al passaggio del mouse */ -} - -.contact-info a:visited { - color: #666; /* Colore grigio scuro per i link visitati */ -} - -/* Stile per i titoli delle sezioni */ -h2 { - font-size: 24px; - color: #0056b3; - border-bottom: 1px solid #d1d1d1; - margin-bottom: 10px; - padding-bottom: 5px; - font-family: 'Montserrat', sans-serif; -} - -/* Stile per le esperienze professionali */ -h3 { - font-size: 20px; - color: #212529; - margin-top: 15px; - margin-bottom: 5px; -} - -em { - color: #555; -} - -ol, ul { - margin-left: 20px; - margin-bottom: 15px; -} - -li { - margin-bottom: 8px; - line-height: 1.5; -} - -/* Stile per le sezioni secondarie */ -p { - margin-bottom: 15px; -} - -b { - color: #212529; -} - -/* Stile per i link */ -a { - color: #0056b3; -} - -a:hover { - color: #ff5722; - text-decoration: underline; -} - -/* Responsività per schermi più piccoli */ -@media (max-width: 768px) { - header { - flex-direction: column; - text-align: center; - } - - .contact-info { - text-align: center; - margin-top: 10px; - } -} - -.markdown code { background:#f0f0f0; color:navy; border-radius:6px; padding:2px; } -.markdown pre { background:#f0f0f0; margin:12px; border:1px solid #ddd; padding:20px 12px; border-radius:6px; } -.markdown pre:hover button { display:block; } -.markdown pre button { display:none; position:relative; float:right; top:-16px } -.markdown blockquote { background:#f0f0f0; border-left:6px solid grey; padding:8px } -.markdown table { margin:12px; border-collapse: collapse; } -.markdown th { border:1px solid grey; background:lightgrey; padding:6px; } -.markdown td { border:1px solid grey; padding:6px; } -.markdown tr:nth-child(even) { background:#f0f0f0; } -.markdown ins { color:#890604 } -.markdown rem { color:#198964 } -.toc ul { padding: 0 12px; } -.toc h3 { color:#0057b7; border-bottom:1px dotted grey } -.toc .H1 { list-style-type:none; font-weight:600; margin:4px; background:#eee } -.toc .H2 { list-style-type:none; font-weight:600; margin:4px; } -.toc .H3 { margin-left:2em } -.toc .H4 { margin-left:4em } -.toc .active { color:#0057b7 } -.toc li:hover { background:#f0f0f0 } \ No newline at end of file diff --git a/strings.py b/strings.py index 7435c20..6d193d5 100644 --- a/strings.py +++ b/strings.py @@ -1,144 +1,3 @@ -resume_markdown_template = """ -Act as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is twofold: - -1. **Review and Extract Information**: Carefully examine the candidate's current resume to extract the following critical details: - - Work experience - - Educational background - - Relevant skills - - Achievements - - Certifications - -2. **Optimize the Resume**: Using the provided template, create a highly optimized resume for the relevant industry. The resume should: - - Include commonly required skills and keywords for the industry - - Utilize ATS-friendly phrases and terminology to ensure compatibility with automated systems - - Highlight strengths and achievements relevant to the industry - - Present experience, skills, and accomplishments in a compelling and professional manner - - Maintain a clear, that is easily readable by both ATS and human reviewers - -Provide guidance on how to enhance the presentation of the information to maximize impact and readability. Offer advice on tailoring the content to general industry standards, ensuring the resume passes ATS filters and captures the attention of recruiters, thereby increasing the candidate’s chances of securing an interview. - -## Information to Collect and Analyze -- **My information eesume:** - {resume} - -## Template to Use -``` -# [Full Name] - -[Your City, Your Country](Maps link) -[Your Prefix Phone number](tel: Your Prefix Phone number) -[Your Email](mailto:Your Email) -[LinkedIn](Link LinkedIn account) -[GitHub](Link GitHub account) - -## Summary - -[Brief professional summary highlighting your experience, key skills, and career objectives. 2-3 sentences.] - -## Skills - -- **Skill1:** [details (max 15 word)] -- **Skill2:** [details (max 15 word)] -- **Skill3:** [details (max 15 word)] -- **Skill4:** [details (max 15 word)] -- **Skill4:** [details (max 15 word)] -- **Skill5:** [details (max 15 word)] - -## Working Experience - -### [Job Title] -**[Company Name]** – [City, State] -*[Start Date – End Date]* - -1. [Achievement or responsibility] -2. [Achievement or responsibility] -3. [Achievement or responsibility] -4. [Achievement or responsibility] -5. [Achievement or responsibility] - -### [Job Title] -**[Company Name]** – [City, State] -*[Start Date – End Date]* - -1. [Achievement or responsibility] -2. [Achievement or responsibility] -3. [Achievement or responsibility] -4. [Achievement or responsibility] -5. [Achievement or responsibility] - -### [Job Title] -**[Company Name]** – [City, State] -*[Start Date – End Date]* - -1. [Achievement or responsibility] -2. [Achievement or responsibility] -3. [Achievement or responsibility] -4. [Achievement or responsibility] -5. [Achievement or responsibility] - -## Education - -**[Degree] in [Field of Study]** -[University Name] – [City, State] -*Graduated: [Month Year]* - -## Certifications - -1. [Certification Name] -2. [Certification Name] -3. [Certification Name] - -## Projects - -### [Project Name] -1. [Brief description of the project and your role] - -### [Project Name] -1. [Brief description of the project and your role] - -### [Project Name] -1. [Brief description of the project and your role] - -## Languages - -1. **[Language]:** [Proficiency Level] -2. **[Language]:** [Proficiency Level] -``` -The results should be provided in **markdown** format, Provide only the markdown code for the resume, without any explanations or additional text and also without ```markdown ``` -""" - -fusion_job_description_resume_template = """ - -Act as an HR expert and resume writer with a strategic approach. Customize the resume to highlight the candidate’s -strengths, skills, and achievements that are most relevant to the provided job description. -Use a smart and targeted approach, incorporating key skills and abilities as well as important aspects of the job -description into the resume. -Ensure that the resume grabs the attention of hiring managers within the first few seconds and uses specific keywords and phrases from the job description to pass through Applicant Tracking Systems (ATS). - -Important Note: While making the necessary adjustments to align the resume with the job description, ensure that the overall structure of the resume remains intact. Do not drastically alter the organization of the document, but optimize it to highlight the most relevant points for the desired position. - -- **Most important infomation on job descrption:** - {job_description} - -- **My information resume:** - {formatted_resume} - -The results should be provided in **markdown** format, Provide only the markdown code for the resume, without any explanations or additional text and also without ```markdown ``` - """ - - - -html_template = """ - -Resume - - - - - -""" - - # Personal Information Template personal_information_template = """ Answer the following question based on the provided personal information. @@ -420,61 +279,74 @@ Please write the cover letter in a way that directly addresses the job role and ``` """ - -resume_stuff_template = """ -The following is a resume, personal data, and an answered question using this information, being answered by the person who's resume it is (first person). - -## Rules -- Answer questions directly -- If seems likely that you have the experience, even if is not explicitly defined, answer as if you have the experience -- If you cannot answer the question, answer things like "I have no experience with that, but I learn fast, very fast", "not yet, but I will learn"... -- The answer must not be longer than a tweet (140 characters) -- Only add periods if the answer has multiple sentences/paragraphs - -## Example 1 -My resume: I'm a software engineer with 10 years of experience in swift . -Question: What is your experience with swift? -10 years - ------ - -## My resume: -``` -{resume} -``` - -## Question: -{question} - -## """ +numeric_question_template = """ +Read the following resume carefully and answer the specific questions regarding the candidate's experience with a number of years. Follow these strategic guidelines when responding: -numeric_question_template = """The following is a resume and an answered question about the resume, being answered by the person who's resume it is (first person). +1. **Related and Inferred Experience:** + - **Similar Technologies:** If experience with a technology is not explicitly stated, but the candidate has experience with similar or related technologies, respond with a number of years that reflects this related experience. For example, if the candidate has experience with Python and projects that involve technologies similar to Java, consider a plausible number of years for Java. + - **Projects and Studies:** Examine the candidate’s projects and studies to infer skills not explicitly mentioned. Complex and advanced projects often indicate deeper expertise. For instance, if a project involves MQTT, you might infer IoT experience even if it's not explicitly mentioned. + +2. **Indirect Experience and Academic Background:** + - **Relevant Projects:** Consider the types of projects the candidate has worked on and the time spent on each project. Advanced projects suggest deeper skills. For example, a project involving MQTT packet parsing suggests MQTT and possibly IoT skills. + - **Roles and Responsibilities:** Evaluate the roles and responsibilities held. If a role suggests knowledge of specific technologies or skills, provide a number based on that experience. + - **Type of University and Studies:** Also consider the type of university and the duration of studies. Prestigious universities and advanced coursework may indicate solid theoretical knowledge. However, give less weight to academic skills compared to practical experience and projects. For example, a degree from a high-level university should influence answers to technical questions minimally. + +3. **Inference Over Default Response:** Always strive to infer experience based on the available information. If direct experience cannot be confirmed, use related skills, projects, and academic background to estimate a plausible number of years. Avoid defaulting to 0 if you can infer any relevant experience. + +4. **Handling Experience Estimates:** + - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience for lower amounts of experience (up to 5 years). Use related skills and projects to estimate these numbers reasonably. + - **For High Experience:** For high levels of experience, ensure the number provided is as certain as possible and based on clear evidence from the resume. Avoid making inferences for high experience levels unless the evidence is strong. + ## Rules - Answer the question directly (only number). -- Regarding work experience just check the Experience Details -> Skills Acquired section. -- Regarding experience in general just check the section Experience Details -> Skills Acquired and also Education Details -> Skills Acquired. -- If it seems likely that you have the experience based on the resume, even if not explicitly stated on the resume, answer as if you have the experience. -- If you cannot answer the question, provide answers like "I have no experience with that, but I learn fast, very fast", "not yet, but I will learn". -- The answer must not be larger than a tweet (140 characters). -## Example -My resume: I'm a software engineer with 10 years of experience on both swift and python. -Question: how much years experience with swift? -10 - ------ - -## My resume: +## Example 1 ``` -{resume} +## Curriculum + +I am a software engineer with 10 years of experience in Swift and Python. I have worked on projects including an i work 2 years with MQTT protocol. + +## Question + +How many years of experience do you have with IoT? + +## Answer + +2 +``` + +## Example 2 +``` +## Curriculum + +I am a software engineer with 5 years of experience in Swift and Python. I have worked on a AI project. + +## Question + +How many years of experience do you have with AI? + +## Answer + +2 +``` + +## Resume: +``` +{resume_educations} +{resume_jobs} +{resume_projects} ``` ## Question: {question} -## """ +--- + +When responding, consider all available information, including projects, work experience, and academic background, to provide an accurate and well-reasoned answer. Make every effort to infer relevant experience and avoid defaulting to 0 if any related experience can be estimated. + +""" options_template = """The following is a resume and an answered question about the resume, the answer is one of the options. diff --git a/utils.py b/utils.py index 45a46e9..bc5cbea 100644 --- a/utils.py +++ b/utils.py @@ -1,15 +1,9 @@ -import json import os import random import time -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.chrome.service import Service as ChromeService -from selenium import webdriver -import time -import glob -from webdriver_manager.chrome import ChromeDriverManager -headless = False +from selenium import webdriver + chromeProfilePath = os.path.join(os.getcwd(), "chrome_profile", "linkedin_profile") def ensure_chrome_profile(): @@ -53,68 +47,36 @@ def scroll_slow(driver, scrollable_element, start=0, end=3600, step=100, reverse except Exception as e: print(f"Exception occurred: {e}") - -def HTML_to_PDF(FilePath): - # Validate and prepare file paths - if not os.path.isfile(FilePath): - raise FileNotFoundError(f"The specified file does not exist: {FilePath}") - FilePath = f"file:///{os.path.abspath(FilePath).replace(os.sep, '/')}" - # Set up Chrome options - chrome_options = webdriver.ChromeOptions() - # Initialize Chrome driver - service = ChromeService(ChromeDriverManager().install()) - driver = webdriver.Chrome(service=service, options=chrome_options) - - try: - # Load the HTML file - driver.get(FilePath) - time.sleep(3) - start_time = time.time() - pdf_base64 = driver.execute_cdp_cmd("Page.printToPDF", { - "printBackground": True, - "landscape": False, - "paperWidth": 10, - "paperHeight": 11, - "marginTop": 0, - "marginBottom": 0, - "marginLeft": 0, - "marginRight": 0, - "displayHeaderFooter": False, - "preferCSSPageSize": True, - "generateDocumentOutline": False, - "generateTaggedPDF": False, - "transferMode": "ReturnAsBase64" - }) - - if time.time() - start_time > 120: - raise TimeoutError("PDF generation exceeded the specified timeout limit.") - return pdf_base64['data'] - - except WebDriverException as e: - raise RuntimeError(f"WebDriver exception occurred: {e}") - - finally: - # Ensure the driver is closed - driver.quit() - def chromeBrowserOptions(): - options = webdriver.ChromeOptions() - options.add_argument('--no-sandbox') - options.add_argument("--ignore-certificate-errors") - options.add_argument("--disable-extensions") - options.add_argument('--disable-gpu') - options.add_argument('--disable-dev-shm-usage') - options.add_argument('--remote-debugging-port=9222') - if headless: - options.add_argument("--headless") - options.add_argument("--start-maximized") - options.add_argument("--disable-blink-features") - options.add_argument("--disable-blink-features=AutomationControlled") - options.add_experimental_option('useAutomationExtension', False) - options.add_experimental_option("excludeSwitches", ["enable-automation"]) - - # Assicurati che la directory del profilo Chrome esista ensure_chrome_profile() + options = webdriver.ChromeOptions() + """options.add_argument("--start-maximized") # Avvia il browser a schermo intero + options.add_argument("--no-sandbox") # Disabilita la sandboxing per migliorare le prestazioni + options.add_argument("--disable-dev-shm-usage") # Utilizza una directory temporanea per la memoria condivisa + options.add_argument("--ignore-certificate-errors") # Ignora gli errori dei certificati SSL + options.add_argument("--disable-extensions") # Disabilita le estensioni del browser + options.add_argument("--disable-gpu") # Disabilita l'accelerazione GPU + options.add_argument("window-size=1200x800") # Imposta la dimensione della finestra del browser + options.add_argument("--disable-background-timer-throttling") # Disabilita il throttling dei timer in background + options.add_argument("--disable-backgrounding-occluded-windows") # Disabilita la sospensione delle finestre occluse + options.add_argument("--disable-translate") # Disabilita il traduttore automatico + options.add_argument("--disable-popup-blocking") # Disabilita il blocco dei popup + options.add_argument("--no-first-run") # Disabilita la configurazione iniziale del browser + options.add_argument("--no-default-browser-check") # Disabilita il controllo del browser predefinito + options.add_argument("--single-process") # Esegui Chrome in un solo processo + options.add_argument("--disable-logging") # Disabilita il logging + options.add_argument("--disable-autofill") # Disabilita l'autocompletamento dei moduli + options.add_argument("--disable-plugins") # Disabilita i plugin del browser + options.add_argument("--disable-animations") # Disabilita le animazioni + options.add_argument("--disable-cache") # Disabilita la cache + options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"]) # Esclude switch della modalità automatica e logging + + # Preferenze per contenuti + prefs = { + "profile.default_content_setting_values.images": 2, # Disabilita il caricamento delle immagini + "profile.managed_default_content_settings.stylesheets": 2, # Disabilita il caricamento dei fogli di stile + } + options.add_experimental_option("prefs", prefs) if len(chromeProfilePath) > 0: initialPath = os.path.dirname(chromeProfilePath) @@ -122,8 +84,11 @@ def chromeBrowserOptions(): options.add_argument('--user-data-dir=' + initialPath) options.add_argument("--profile-directory=" + profileDir) else: - options.add_argument("--incognito") - + options.add_argument("--incognito")""" + initialPath = os.path.dirname(chromeProfilePath) + profileDir = os.path.basename(chromeProfilePath) + options.add_argument('--user-data-dir=' + initialPath) + options.add_argument("--profile-directory=" + profileDir) return options From f4f5abfa2c039f4e0af6f10bb1838c0cbb633541 Mon Sep 17 00:00:00 2001 From: Federico <85809106+feder-cr@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:44:25 +0100 Subject: [PATCH 02/23] Update plain_text_resume.yaml --- data_folder/plain_text_resume.yaml | 206 +++++++++++++++++------------ 1 file changed, 118 insertions(+), 88 deletions(-) diff --git a/data_folder/plain_text_resume.yaml b/data_folder/plain_text_resume.yaml index 716d633..c0ad978 100644 --- a/data_folder/plain_text_resume.yaml +++ b/data_folder/plain_text_resume.yaml @@ -1,103 +1,133 @@ personal_information: - name: "[Your name]" - surname: "[Your surname]" - dateOfBirth: "[Your Date of Birth]" - country: "[Your country]" - city: "[Your city]" - address: "[Your address]" - phonePrefix: "[Your phone number prefix]" - phone: "[Your phone number]" - email: "[Your email]" - github: "[Link]" - linkedin: "[Link]" - -self_identification: - gender: "[Specific gender identification]" - pronouns: "[Your Pronouns]" - veteran: [true/false] - disability: [true/false] - ethnicity: "[Specify ethnicity]" - -legal_authorization: - euWorkAuthorization: [true/false] - usWorkAuthorization: [true/false] - requiresUsVisa: [true/false] - legallyAllowedToWorkInUs: [true/false] - requiresUsSponsorship: [true/false] - requiresEuVisa: [true/false] - legallyAllowedToWorkInEu: [true/false] - requiresEuSponsorship: [true/false] - -work_preferences: - remoteWork: [true/false] - inPersonWork: [true/false] - openToRelocation: [true/false] - willingToCompleteAssessments: [true/false] - willingToUndergoDrugTests: [true/false] - willingToUndergoBackgroundChecks: [true/false] - + name: "Liam" + surname: "Murphy" + date_of_birth: "15/08/1995" + country: "Ireland" + city: "Galway" + address: "Galway City Center" + phone_prefix: "+353" + phone: "871234567" + email: "liam.murphy@gmail.com" + github: "https://github.com/liam-murphy" + linkedin: "https://www.linkedin.com/in/liam-murphy/" + education_details: - - degree: "[Bachelor's/Master's/Ph.D.]" - university: "[Name of University]" - gpa: "[Your GPA]" - graduationYear: "[Year of Graduation]" - fieldOfStudy: "[Your Field of Study]" - skillsAcquired: - leadership: "[Years]" - problemSolving: "[Years]" - criticalThinking: "[Years]" - adaptability: "[Years]" - perfectionism: "[Years]" - yourSkill: "[Years]" - yourSkill: "[Years]" + - degree: "Bachelor's Degree" + university: "National University of Ireland, Galway" + gpa: "4/4" + graduation_year: "2020" + field_of_study: "Computer Science" + exam: + Information Theory and Inference: "4" + Algorithm Analysis and Design: "4" + Object-Oriented Languages and Programming: "4" + Linear Algebra and Numerical Analysis: "4" + Database: "4" experience_details: - - position: "[Job Title]" - company: "[Company Name]" - employmentPeriod: "[Month, Year] - [Month, Year]" - location: "[City, Country]" - industry: "[Industry of the Company]" - keyResponsibilities: - responsibility1: "[Description of responsibility1]" - responsibility2: "[Description of responsibility1]" - responsibility3: "[Description of responsibility1]" - skillsAcquired: - leadership: "[Years]" - problemSolving: "[Years]" - criticalThinking: "[Years]" - adaptability: "[Years]" - perfectionism: "[Years]" - yourSkill: "[Years]" - yourSkill: "[Years]" + - position: "Co-Founder & Software Engineer" + company: "CryptoWave Solutions" + employment_period: "03/2021 - Present" + location: "Ireland" + industry: "Blockchain Technology" + key_responsibilities: + - responsibility_1: "Co-founded and led a startup specializing in app and software development with a focus on blockchain technology" + - responsibility_2: "Provided blockchain consultations for 10+ companies, enhancing their software capabilities with secure, decentralized solutions" + - responsibility_3: "Developed blockchain applications, integrated cutting-edge technology to meet client needs and drive industry innovation" + skills_acquired: + - "Blockchain development" + - "Software engineering" + - "Consultancy" + + - position: "Research Intern" + company: "National University of Ireland, Galway" + employment_period: "11/2022 - 03/2023" + location: "Galway, Ireland" + industry: "IoT Security Research" + key_responsibilities: + - responsibility_1: "Conducted in-depth research on IoT security, focusing on binary instrumentation and runtime monitoring" + - responsibility_2: "Performed in-depth study of the MQTT protocol and Falco" + - responsibility_3: "Developed multiple software components including MQTT packet analysis library, Falco adapter, and RML monitor in Prolog" + - responsibility_4: "Authored thesis 'Binary Instrumentation for Runtime Monitoring of Internet of Things Systems Using Falco'" + skills_acquired: + - "IoT security" + - "Binary instrumentation" + - "MQTT protocol" + - "Prolog programming" + + - position: "Software Engineer" + company: "University Hospital Galway" + employment_period: "05/2022 - 11/2022" + location: "Galway, Ireland" + industry: "Healthcare IT" + key_responsibilities: + - responsibility_1: "Integrated and enforced robust security protocols" + - responsibility_2: "Developed and maintained a critical software tool for password validation used by over 1,600 employees" + - responsibility_3: "Played an integral role in the hospital's cybersecurity team" + skills_acquired: + - "Cybersecurity" + - "Software development" + - "Password validation" projects: - project1: "[Description of significant projects you've worked on + if available repo link]" - project2: "[Description of significant projects you've worked on + if available repo link]" + - name: "JobBot" + description: "AI-driven tool to automate and personalize job applications on LinkedIn, gained over 3000 stars on GitHub, improving efficiency and reducing application time" + link: "https://github.com/liam-murphy/jobbot" + - name: "mqtt-packet-parser" + description: "Developed a Node.js module for parsing MQTT packets, improved parsing efficiency by 40%" + link: "https://github.com/liam-murphy/mqtt-packet-parser" -availability: - noticePeriod: "[Specify notice period]" - -salary_expectations: - salaryRangeUSD: "[Specify your salary expectations in USD]" +achievements: + - name: "Winner of an Irish public competition" + description: "Won first place in a public competition with a perfect score of 70/70, securing a Software Developer position at University Hospital Galway" + - name: "Galway Merit Scholarship" + description: "Awarded annually from 2018 to 2020 in recognition of academic excellence and contribution" + - name: "GitHub Recognition" + description: "Gained over 3000 stars on GitHub with JobBot project" certifications: - - "[Certification 1]" - - "[Certification 2]" - - "[Certification 3]" - -skills: - problemSolving: "[Years]" - criticalThinking: "[Years]" - adaptability: "[Years]" - perfectionism: "[Years]" - yourSkill: "[Years]" - yourSkill: "[Years]" + - "C1" languages: - - language: "Italian" - proficiency: "Native" - language: "English" + proficiency: "Native" + - language: "Spanish" proficiency: "Professional" interests: - - "[List any hobbies or interests relevant to your professional profile]" + - "Full-Stack Development" + - "Software Architecture" + - "IoT system design and development" + - "Artificial Intelligence" + - "Cloud Technologies" + +availability: + notice_period: "immediately" + +salary_expectations: + salary_range_usd: "100000" + +self_identification: + gender: "Male" + pronouns: "He" + veteran: "No" + disability: "No" + ethnicity: "white" + +legal_authorization: + eu_work_authorization: "Yes" + us_work_authorization: "No" + requires_us_visa: "Yes" + requires_us_sponsorship: "Yes" + requires_eu_visa: "No" + legally_allowed_to_work_in_eu: "Yes" + legally_allowed_to_work_in_us: "No" + requires_eu_sponsorship: "No" + +work_preferences: + remote_work: "Yes" + in_person_work: "Yes" + open_to_relocation: "Yes" + willing_to_complete_assessments: "Yes" + willing_to_undergo_drug_tests: "Yes" + willing_to_undergo_background_checks: "Yes" From aad7cbdf9e2320ef503ef163905be43a4a8dae6d Mon Sep 17 00:00:00 2001 From: Federico <85809106+feder-cr@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:49:29 +0100 Subject: [PATCH 03/23] Update config.yaml --- data_folder/config.yaml | 51 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/data_folder/config.yaml b/data_folder/config.yaml index 3a51b0f..969c6b1 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -1,41 +1,42 @@ -remote: [true/false] +remote: true experienceLevel: - internship: [true/false] - entry: [true/false] - associate: [true/false] - mid-senior level: [true/false] - director: [true/false] - executive: [true/false] + internship: true + entry: false + associate: true + mid-senior level: false + director: true + executive: false jobTypes: - full-time: [true/false] - contract: [true/false] - part-time: [true/false] - temporary: [true/false] - internship: [true/false] - other: [true/false] - volunteer: [true/false] + full-time: true + contract: false + part-time: false + temporary: true + internship: false + other: false + volunteer: true date: - all time: [true/false] - month: [true/false] - week: [true/false] - 24 hours: [true/false] + all time: false + month: true + week: false + 24 hours: true positions: - - Software developer + - AI Software Enginner + - Software Enginner ML + - AI Enginner + - AI Software Enginner locations: - - Country1 - - Country2 + - USA distance: 100 companyBlacklist: - - Company1 - - Company2 + - Noir + - Crossover titleBlacklist: - - word1 - - word2 + - diocane From b604b6328b1a14ad4731713dae98f0b7b4341a44 Mon Sep 17 00:00:00 2001 From: Federico <85809106+feder-cr@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:49:45 +0100 Subject: [PATCH 04/23] Update config.yaml --- data_folder/config.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/data_folder/config.yaml b/data_folder/config.yaml index 969c6b1..ca23ac6 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -24,10 +24,8 @@ date: 24 hours: true positions: - - AI Software Enginner - Software Enginner ML - - AI Enginner - - AI Software Enginner + locations: - USA From 8389e9194cc16b66e525cdb53e0fbec4743c1eef Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 10:54:25 +0100 Subject: [PATCH 05/23] lib path fix --- main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/main.py b/main.py index 065fc52..0253c97 100644 --- a/main.py +++ b/main.py @@ -8,9 +8,6 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager 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 30a78b6715290101ad215d0f8111de2318f946c4 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 13:25:08 +0100 Subject: [PATCH 06/23] fix req --- requirements.txt | Bin 834 -> 688 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index b7ba6dc32928a82aa24cba0d1836b795225a47c4..bbe9375cee953eaa266eddd77baf945f985762b8 100644 GIT binary patch delta 7 OcmX@awt;oS1||RuZvx-| delta 14 VcmdnMdWdbq2ByghOcR(A7yu|f1cv|s From af41201b4f3a85bb8d5ba3aa6b5afd16604962a5 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 16:12:03 +0100 Subject: [PATCH 07/23] fix path resume problem --- linkedIn_easy_applier.py | 12 +++++------- linkedIn_job_manager.py | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index bf931b3..ae60432 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -23,7 +23,7 @@ class LinkedInEasyApplier: if resume_dir is None or not os.path.exists(resume_dir): resume_dir = None self.driver = driver - self.resume_dir = resume_dir + self.resume_path = resume_dir self.set_old_answers = set_old_answers self.gpt_answerer = gpt_answerer self.resume_generator_manager = resume_generator_manager @@ -154,12 +154,10 @@ class LinkedInEasyApplier: self.driver.execute_script("arguments[0].classList.remove('hidden')", element) output = self.gpt_answerer.resume_or_cover(parent.text.lower()) if 'resume' in output: - if self.resume_dir: - resume_path = self.resume_dir.resolve() - if resume_path.exists() and resume_path.is_file(): - element.send_keys(str(resume_path)) - else: - self._create_and_upload_resume(element, job) + if self.resume_path is not None and self.resume_path.resolve().is_file(): + element.send_keys(str(self.resume_path.resolve())) + else: + self._create_and_upload_resume(element, job) elif 'cover' in output: self._create_and_upload_cover_letter(element) diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index af17877..bb36f7f 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -40,9 +40,9 @@ class LinkedInJobManager: self.seen_jobs = [] resume_path = parameters.get('uploads', {}).get('resume', None) if resume_path is not None and Path(resume_path).exists(): - self.resume_dir = Path(resume_path) + self.resume_path = Path(resume_path) else: - self.resume_dir = None + self.resume_path = None self.output_file_directory = Path(parameters['outputFileDirectory']) self.env_config = EnvironmentKeys() #self.old_question() @@ -66,7 +66,7 @@ class LinkedInJobManager: def start_applying(self): - self.easy_applier_component = LinkedInEasyApplier(self.driver, self.resume_dir, self.set_old_answers, self.gpt_answerer, self.resume_generator_manager) + self.easy_applier_component = LinkedInEasyApplier(self.driver, self.resume_path, self.set_old_answers, self.gpt_answerer, self.resume_generator_manager) searches = list(product(self.positions, self.locations)) random.shuffle(searches) page_sleep = 0 From 78d0a8ebca68be973aec84cf0aed837154090ed8 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 16:54:56 +0100 Subject: [PATCH 08/23] is_numeric fixed --- linkedIn_easy_applier.py | 9 ++++--- linkedIn_job_manager.py | 58 +++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index ae60432..2160f6a 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -253,9 +253,12 @@ class LinkedInEasyApplier: return True return False - def _is_numeric_field(self, element: WebElement) -> bool: - input_type = element.get_attribute('type') - return input_type == 'number' + def _is_numeric_field(self, field: WebElement) -> bool: + field_type = field.get_attribute('type').lower() + if 'numeric' in field_type: + return True + class_attribute = field.get_attribute("id") + return class_attribute and 'numeric' in class_attribute def _enter_text(self, element: WebElement, text: str) -> None: element.clear() diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index bb36f7f..b25bae0 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -115,37 +115,34 @@ class LinkedInJobManager: def apply_jobs(self): try: - try: - no_jobs_element = self.driver.find_element(By.CLASS_NAME, 'jobs-search-two-pane__no-results-banner--expand') - if 'No matching jobs found' in no_jobs_element.text or 'unfortunately, things aren' in self.driver.page_source.lower(): - raise Exception("No more jobs on this page") - except NoSuchElementException: - pass - - job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list") - utils.scroll_slow(self.driver, job_results) - utils.scroll_slow(self.driver, job_results, step=300, reverse=True) - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') - if not job_list_elements: - raise Exception("No job class elements found on page") - job_list = [Job(*self.extract_job_information_from_tile(job_element)) for job_element in job_list_elements] - for job in job_list: - if self.is_blacklisted(job.title, job.company, job.link): - utils.printyellow(f"Blacklisted {job.title} at {job.company}, skipping...") - self.write_to_file(job, "skipped") - continue - try: - if job.apply_method not in {"Continue", "Applied", "Apply"}: - self.easy_applier_component.job_apply(job) - self.write_to_file(job, "success") - except Exception as e: - utils.printred(traceback.format_exc()) - self.write_to_file(job, "failed") - continue + no_jobs_element = self.driver.find_element(By.CLASS_NAME, 'jobs-search-two-pane__no-results-banner--expand') + if 'No matching jobs found' in no_jobs_element.text or 'unfortunately, things aren' in self.driver.page_source.lower(): + raise Exception("No more jobs on this page") + except NoSuchElementException: + pass - except Exception as e: - traceback.format_exc() - raise e + job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list") + utils.scroll_slow(self.driver, job_results) + utils.scroll_slow(self.driver, job_results, step=300, reverse=True) + job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') + if not job_list_elements: + raise Exception("No job class elements found on page") + job_list = [Job(*self.extract_job_information_from_tile(job_element)) for job_element in job_list_elements] + for job in job_list: + if self.is_blacklisted(job.title, job.company, job.link): + utils.printyellow(f"Blacklisted {job.title} at {job.company}, skipping...") + self.write_to_file(job, "skipped") + continue + try: + if job.apply_method not in {"Continue", "Applied", "Apply"}: + self.easy_applier_component.job_apply(job) + self.write_to_file(job, "success") + except Exception as e: + utils.printred(traceback.format_exc()) + self.write_to_file(job, "failed") + continue + + def write_to_file(self, job, file_name): data = { @@ -156,6 +153,7 @@ class LinkedInJobManager: "pdf_path": job.pdf_path } file_path = self.output_file_directory / f"{file_name}.json" + file_path = file_path.as_posix() if not file_path.exists(): with open(file_path, 'w', encoding='utf-8') as f: json.dump([data], f, indent=4) From 737d9cf45759a2dff6b5f9aeba050fe392ea1b68 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 18:24:37 +0100 Subject: [PATCH 09/23] dropdown fixed --- gpt.py | 3 --- linkedIn_easy_applier.py | 31 +++++++++++++++++-------------- linkedIn_job_manager.py | 2 +- main.py | 14 +++++++------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/gpt.py b/gpt.py index 515a82f..368572c 100644 --- a/gpt.py +++ b/gpt.py @@ -267,9 +267,6 @@ class GPTAnswerer: Provide only the exact name of the section from the list above with no additional text. """ - - - prompt = ChatPromptTemplate.from_template(section_prompt) chain = prompt | self.llm_cheap | StrOutputParser() output = chain.invoke({"question": question}) diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index 2160f6a..759e506 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -191,7 +191,9 @@ class LinkedInEasyApplier: def _fill_additional_questions(self) -> None: form_sections = self.driver.find_elements(By.CLASS_NAME, 'jobs-easy-apply-form-section__grouping') for section in form_sections: + outer_html = section.get_attribute('outerHTML') self._process_form_section(section) + def _process_form_section(self, section: WebElement) -> None: if self._handle_terms_of_service(section): @@ -243,15 +245,18 @@ class LinkedInEasyApplier: return False def _find_and_handle_dropdown_question(self, section: WebElement) -> bool: - dropdowns = section.find_elements(By.CLASS_NAME, 'fb-dropdown__select') - if dropdowns: - dropdown = dropdowns[0] - question_text = section.text.lower() - select = Select(dropdown) - answer = self.gpt_answerer.answer_question_from_options(question_text, [option.text.lower() for option in select.options]) - self._select_dropdown_option(select, answer) - return True - return False + try: + question = section.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element') + question_text = question.find_element(By.TAG_NAME, 'label').text.lower() + dropdown = question.find_element(By.TAG_NAME, 'select') + if dropdown: + select = Select(dropdown) + options = [option.text for option in select.options] + answer = self.gpt_answerer.answer_question_from_options(question_text, options) + self._select_dropdown_option(dropdown, answer) + return True + except Exception: + return False def _is_numeric_field(self, field: WebElement) -> bool: field_type = field.get_attribute('type').lower() @@ -270,8 +275,6 @@ class LinkedInEasyApplier: radio.click() break - def _select_dropdown_option(self, select: Select, answer: str) -> None: - for option in select.options: - if option.text.lower() == answer.lower(): - select.select_by_visible_text(option.text) - break + def _select_dropdown_option(self, element: WebElement, text: str) -> None: + select = Select(element) + select.select_by_visible_text(text) diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index b25bae0..2a0bee9 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -153,8 +153,8 @@ class LinkedInJobManager: "pdf_path": job.pdf_path } file_path = self.output_file_directory / f"{file_name}.json" - file_path = file_path.as_posix() if not file_path.exists(): + job.pdf_path = file_path.as_posix() with open(file_path, 'w', encoding='utf-8') as f: json.dump([data], f, indent=4) else: diff --git a/main.py b/main.py index 0253c97..38d99a1 100644 --- a/main.py +++ b/main.py @@ -149,6 +149,12 @@ def init_browser() -> webdriver.Chrome: def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str): try: + + style_manager = StyleManager() + resume_generator = ResumeGenerator() + resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output")) + + browser = init_browser() login_component = LinkedInAuthenticator(browser) @@ -161,13 +167,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k 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_job_application_profile_and_resume(job_application_profile_object, resume_object) From ff07741e75c1aeef11d2e76a3ffe30d00eabc676 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 18:26:00 +0100 Subject: [PATCH 10/23] order main fixed --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 38d99a1..135a632 100644 --- a/main.py +++ b/main.py @@ -152,6 +152,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k style_manager = StyleManager() resume_generator = ResumeGenerator() + resume_object = Resume(plain_text_resume) resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output")) @@ -164,7 +165,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k with open(parameters['uploads']['plainTextResume'], "r") as file: plain_text_resume = file.read() - resume_object = Resume(plain_text_resume) + job_application_profile_object = JobApplicationProfile(plain_text_resume) From 78c7ffbe17c8c7508a826b1eb4bf5748760cc57a Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 18:31:59 +0100 Subject: [PATCH 11/23] now is fixed --- main.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index 135a632..4472e34 100644 --- a/main.py +++ b/main.py @@ -149,26 +149,20 @@ def init_browser() -> webdriver.Chrome: def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str): try: - style_manager = StyleManager() resume_generator = ResumeGenerator() + with open(parameters['uploads']['plainTextResume'], "r") as file: + plain_text_resume = file.read() resume_object = Resume(plain_text_resume) 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() + job_application_profile_object = JobApplicationProfile(plain_text_resume) - 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.read() - - - job_application_profile_object = JobApplicationProfile(plain_text_resume) - - bot = LinkedInBotFacade(login_component, apply_component) bot.set_secrets(email, password) bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object) From 660bb63f0470ec9cfb9d78aaea5d10f445ccb186 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 18:33:02 +0100 Subject: [PATCH 12/23] add clear after chosestyle --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 4472e34..8da7cb1 100644 --- a/main.py +++ b/main.py @@ -157,6 +157,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k 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() + os.system('cls' if os.name == 'nt' else 'clear') job_application_profile_object = JobApplicationProfile(plain_text_resume) browser = init_browser() From 668135d669b038faace287d20a87bd73799ff091 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Thu, 22 Aug 2024 19:33:25 +0100 Subject: [PATCH 13/23] radio fixed --- linkedIn_easy_applier.py | 11 +++++++---- linkedIn_job_manager.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index 759e506..27bee3a 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -215,7 +215,8 @@ class LinkedInEasyApplier: return False def _find_and_handle_radio_question(self, section: WebElement) -> bool: - radios = section.find_elements(By.CLASS_NAME, 'fb-text-selectable__option') + question = section.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element') + radios = question.find_elements(By.CLASS_NAME, 'fb-text-selectable__option') if radios: question_text = section.text.lower() options = [radio.text.lower() for radio in radios] @@ -224,6 +225,7 @@ class LinkedInEasyApplier: return True return False + def _find_and_handle_textbox_question(self, section: WebElement) -> bool: text_fields = section.find_elements(By.TAG_NAME, 'input') + section.find_elements(By.TAG_NAME, 'textarea') if text_fields: @@ -271,9 +273,10 @@ class LinkedInEasyApplier: def _select_radio(self, radios: List[WebElement], answer: str) -> None: for radio in radios: - if radio.text.lower() == answer.lower(): - radio.click() - break + if answer in radio.text.lower(): + radio.find_element(By.TAG_NAME, 'label').click() + return + radios[-1].find_element(By.TAG_NAME, 'label').click() def _select_dropdown_option(self, element: WebElement, text: str) -> None: select = Select(element) diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index 2a0bee9..45a8a52 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -145,16 +145,16 @@ class LinkedInJobManager: def write_to_file(self, job, file_name): + pdf_path = Path(job.pdf_path).as_uri() data = { "company": job.company, "job_title": job.title, "link": job.link, "job_location": job.location, - "pdf_path": job.pdf_path + "pdf_path": pdf_path } file_path = self.output_file_directory / f"{file_name}.json" if not file_path.exists(): - job.pdf_path = file_path.as_posix() with open(file_path, 'w', encoding='utf-8') as f: json.dump([data], f, indent=4) else: From 4fea7833fb608111af96322d8efa7b7254465c44 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 14:51:17 +0100 Subject: [PATCH 14/23] log fix --- linkedIn_job_manager.py | 3 ++- main.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index 45a8a52..79dfece 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -145,7 +145,8 @@ class LinkedInJobManager: def write_to_file(self, job, file_name): - pdf_path = Path(job.pdf_path).as_uri() + pdf_path = Path(job.pdf_path).resolve() + pdf_path = pdf_path.as_uri() data = { "company": job.company, "job_title": job.title, diff --git a/main.py b/main.py index 8da7cb1..1679bf7 100644 --- a/main.py +++ b/main.py @@ -158,6 +158,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k os.system('cls' if os.name == 'nt' else 'clear') resume_generator_manager.choose_style() os.system('cls' if os.name == 'nt' else 'clear') + job_application_profile_object = JobApplicationProfile(plain_text_resume) browser = init_browser() From 74f9f181504cd2f678aed90b1760ed2a5fcf500e Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 15:11:44 +0100 Subject: [PATCH 15/23] cofing problem fixed, i hope :) --- linkedIn_job_manager.py | 2 -- main.py | 24 ++++++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/linkedIn_job_manager.py b/linkedIn_job_manager.py index 79dfece..e6f2940 100644 --- a/linkedIn_job_manager.py +++ b/linkedIn_job_manager.py @@ -142,8 +142,6 @@ class LinkedInJobManager: self.write_to_file(job, "failed") continue - - def write_to_file(self, job, file_name): pdf_path = Path(job.pdf_path).resolve() pdf_path = pdf_path.as_uri() diff --git a/main.py b/main.py index 1679bf7..6c32516 100644 --- a/main.py +++ b/main.py @@ -37,10 +37,10 @@ class ConfigValidator: except FileNotFoundError: raise ConfigError(f"File not found: {yaml_path}") - @staticmethod + + def validate_config(config_yaml_path: Path) -> dict: parameters = ConfigValidator.validate_yaml_file(config_yaml_path) - required_keys = { 'remote': bool, 'experienceLevel': dict, @@ -52,10 +52,18 @@ class ConfigValidator: '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}") + if key not in parameters: + if key in ['companyBlacklist', 'titleBlacklist']: + parameters[key] = [] + else: + 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: + parameters[key] = [] + else: + raise ConfigError(f"Invalid type for key '{key}' in config file {config_yaml_path}. Expected {expected_type}.") experience_levels = ['internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive'] for level in experience_levels: @@ -82,11 +90,15 @@ class ConfigValidator: raise ConfigError(f"Invalid distance value in config file {config_yaml_path}. Must be one of: {approved_distances}") for blacklist in ['companyBlacklist', 'titleBlacklist']: - if not all(isinstance(item, str) for item in parameters.get(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 + + @staticmethod def validate_secrets(secrets_yaml_path: Path) -> tuple: secrets = ConfigValidator.validate_yaml_file(secrets_yaml_path) From 9cff8bf6ee4c52f6ad15d1fdfa0aeda4be6f38e4 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 15:45:05 +0100 Subject: [PATCH 16/23] more inference --- strings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings.py b/strings.py index 6d193d5..cde924a 100644 --- a/strings.py +++ b/strings.py @@ -295,7 +295,7 @@ Read the following resume carefully and answer the specific questions regarding 3. **Inference Over Default Response:** Always strive to infer experience based on the available information. If direct experience cannot be confirmed, use related skills, projects, and academic background to estimate a plausible number of years. Avoid defaulting to 0 if you can infer any relevant experience. 4. **Handling Experience Estimates:** - - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience for lower amounts of experience (up to 5 years). Use related skills and projects to estimate these numbers reasonably. + - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Use related skills and projects to estimate these numbers reasonably. Aim to keep the values as high as possible and avoid using "0" as a response unless absolutely necessary. - **For High Experience:** For high levels of experience, ensure the number provided is as certain as possible and based on clear evidence from the resume. Avoid making inferences for high experience levels unless the evidence is strong. @@ -306,7 +306,7 @@ Read the following resume carefully and answer the specific questions regarding ``` ## Curriculum -I am a software engineer with 10 years of experience in Swift and Python. I have worked on projects including an i work 2 years with MQTT protocol. +I am a software engineer with 3 years of experience in Swift and Python. I have worked on projects including an i work 2 years with MQTT protocol. ## Question From 3c09856232e6ab5822032769ea293f9db7060f48 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 18:30:48 +0100 Subject: [PATCH 17/23] lint --- linkedIn_authenticator.py | 7 ---- linkedIn_easy_applier.py | 71 +++++++++++++++++++++++++++++++++++++-- main.py | 3 +- strings.py | 20 +++++++++-- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/linkedIn_authenticator.py b/linkedIn_authenticator.py index 590cfcc..c953e5a 100644 --- a/linkedIn_authenticator.py +++ b/linkedIn_authenticator.py @@ -16,7 +16,6 @@ class LinkedInAuthenticator: self.password = password def start(self): - """Start the Chrome browser and attempt to log in to LinkedIn.""" print("Starting Chrome browser to log in to LinkedIn.") self.driver.get('https://www.linkedin.com') self.wait_for_page_load() @@ -24,7 +23,6 @@ class LinkedInAuthenticator: self.handle_login() def handle_login(self): - """Handle the LinkedIn login process.""" print("Navigating to the LinkedIn login page...") self.driver.get("https://www.linkedin.com/login") try: @@ -36,7 +34,6 @@ class LinkedInAuthenticator: self.handle_security_check() def enter_credentials(self): - """Enter the user's email and password into the login form.""" try: email_field = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.ID, "username")) @@ -48,7 +45,6 @@ class LinkedInAuthenticator: print("Login form not found. Aborting login.") def submit_login_form(self): - """Submit the LinkedIn login form.""" try: login_button = self.driver.find_element(By.XPATH, '//button[@type="submit"]') login_button.click() @@ -56,7 +52,6 @@ class LinkedInAuthenticator: print("Login button not found. Please verify the page structure.") def handle_security_check(self): - """Handle LinkedIn security checks if triggered.""" try: WebDriverWait(self.driver, 10).until( EC.url_contains('https://www.linkedin.com/checkpoint/challengesV2/') @@ -70,7 +65,6 @@ class LinkedInAuthenticator: print("Security check not completed. Please try again later.") def is_logged_in(self): - """Check if the user is already logged in to LinkedIn.""" self.driver.get('https://www.linkedin.com/feed') try: WebDriverWait(self.driver, 10).until( @@ -85,7 +79,6 @@ class LinkedInAuthenticator: return False def wait_for_page_load(self, timeout=10): - """Wait for the page to fully load.""" try: WebDriverWait(self.driver, timeout).until( lambda d: d.execute_script('return document.readyState') == 'complete' diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index 27bee3a..f51b578 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -1,4 +1,5 @@ import base64 +import json import os import random import re @@ -27,6 +28,31 @@ class LinkedInEasyApplier: self.set_old_answers = set_old_answers self.gpt_answerer = gpt_answerer self.resume_generator_manager = resume_generator_manager + self.questions_data = [] + + + def _load_questions_from_json(self) -> List[dict]: + output_file = 'answers.json' + try: + # Leggi i dati esistenti dal file + try: + with open(output_file, 'r') as f: + try: + all_data = json.load(f) + if not isinstance(all_data, list): + raise ValueError("JSON file format is incorrect. Expected a list of questions.") + except json.JSONDecodeError: + # Se il file è vuoto o non contiene JSON valido, inizializza come lista vuota + all_data = [] + except FileNotFoundError: + # Se il file non esiste, inizializza come lista vuota + all_data = [] + + return all_data + except Exception: + tb_str = traceback.format_exc() + raise Exception(f"Error loading questions data from JSON file: \nTraceback:\n{tb_str}") + def job_apply(self, job: Any): self.driver.get(job.link) @@ -191,7 +217,6 @@ class LinkedInEasyApplier: def _fill_additional_questions(self) -> None: form_sections = self.driver.find_elements(By.CLASS_NAME, 'jobs-easy-apply-form-section__grouping') for section in form_sections: - outer_html = section.get_attribute('outerHTML') self._process_form_section(section) @@ -222,6 +247,7 @@ class LinkedInEasyApplier: options = [radio.text.lower() for radio in radios] answer = self.gpt_answerer.answer_question_from_options(question_text, options) self._select_radio(radios, answer) + self._save_questions_to_json({'type': 'radio', 'question': question_text, 'answer': answer}) return True return False @@ -232,8 +258,14 @@ class LinkedInEasyApplier: text_field = text_fields[0] question_text = section.find_element(By.TAG_NAME, 'label').text.lower() is_numeric = self._is_numeric_field(text_field) - answer = self.gpt_answerer.answer_question_numeric(question_text) if is_numeric else self.gpt_answerer.answer_question_textual_wide_range(question_text) + if is_numeric: + answer = self.gpt_answerer.answer_question_numeric(question_text) + question_type = 'numeric' + else: + answer = self.gpt_answerer.answer_question_textual_wide_range(question_text) + question_type = 'textbox' self._enter_text(text_field, answer) + self._save_questions_to_json({'type': question_type, 'question': question_text, 'answer': answer}) return True return False @@ -243,6 +275,7 @@ class LinkedInEasyApplier: date_field = date_fields[0] answer_date = self.gpt_answerer.answer_question_date() self._enter_text(date_field, answer_date.strftime("%Y-%m-%d")) + self._save_questions_to_json({'type': 'date', 'question': section.text.lower(), 'answer': answer_date.strftime("%Y-%m-%d")}) return True return False @@ -256,6 +289,7 @@ class LinkedInEasyApplier: options = [option.text for option in select.options] answer = self.gpt_answerer.answer_question_from_options(question_text, options) self._select_dropdown_option(dropdown, answer) + self._save_questions_to_json({'type': 'dropdown', 'question': question_text, 'answer': answer}) return True except Exception: return False @@ -281,3 +315,36 @@ class LinkedInEasyApplier: def _select_dropdown_option(self, element: WebElement, text: str) -> None: select = Select(element) select.select_by_visible_text(text) + + def _save_questions_to_json(self, question_data: dict) -> None: + output_file = 'answers.json' + question_data['question'] = self._sanitize_text(question_data['question']) + try: + try: + with open(output_file, 'r') as f: + try: + all_data = json.load(f) + if not isinstance(all_data, list): + raise ValueError("JSON file format is incorrect. Expected a list of questions.") + except json.JSONDecodeError: + all_data = [] + except FileNotFoundError: + all_data = [] + all_data.append(question_data) + with open(output_file, 'w') as f: + json.dump(all_data, f, indent=4) + except Exception: + tb_str = traceback.format_exc() + raise Exception(f"Error saving questions data to JSON file: \nTraceback:\n{tb_str}") + + + + def _sanitize_text(self, text: str) -> str: + sanitized_text = text.lower() + sanitized_text = sanitized_text.strip() + sanitized_text = sanitized_text.replace('"', '') + sanitized_text = sanitized_text.replace('\\', '') + sanitized_text = re.sub(r'[\x00-\x1F\x7F]', '', sanitized_text) + sanitized_text = sanitized_text.replace('\n', ' ').replace('\r', '') + sanitized_text = sanitized_text.rstrip(',') + return sanitized_text \ No newline at end of file diff --git a/main.py b/main.py index 6c32516..2028866 100644 --- a/main.py +++ b/main.py @@ -37,8 +37,7 @@ class ConfigValidator: except FileNotFoundError: raise ConfigError(f"File not found: {yaml_path}") - - + def validate_config(config_yaml_path: Path) -> dict: parameters = ConfigValidator.validate_yaml_file(config_yaml_path) required_keys = { diff --git a/strings.py b/strings.py index cde924a..557ab5b 100644 --- a/strings.py +++ b/strings.py @@ -290,12 +290,12 @@ Read the following resume carefully and answer the specific questions regarding 2. **Indirect Experience and Academic Background:** - **Relevant Projects:** Consider the types of projects the candidate has worked on and the time spent on each project. Advanced projects suggest deeper skills. For example, a project involving MQTT packet parsing suggests MQTT and possibly IoT skills. - **Roles and Responsibilities:** Evaluate the roles and responsibilities held. If a role suggests knowledge of specific technologies or skills, provide a number based on that experience. - - **Type of University and Studies:** Also consider the type of university and the duration of studies. Prestigious universities and advanced coursework may indicate solid theoretical knowledge. However, give less weight to academic skills compared to practical experience and projects. For example, a degree from a high-level university should influence answers to technical questions minimally. + - **Type of University and Studies:** Also consider the type of university and the duration of studies. 3. **Inference Over Default Response:** Always strive to infer experience based on the available information. If direct experience cannot be confirmed, use related skills, projects, and academic background to estimate a plausible number of years. Avoid defaulting to 0 if you can infer any relevant experience. 4. **Handling Experience Estimates:** - - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Use related skills and projects to estimate these numbers reasonably. Aim to keep the values as high as possible and avoid using "0" as a response unless absolutely necessary. + - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Aim to keep the values as high as possible and avoid using "0" as a response unless absolutely necessary. - **For High Experience:** For high levels of experience, ensure the number provided is as certain as possible and based on clear evidence from the resume. Avoid making inferences for high experience levels unless the evidence is strong. @@ -306,7 +306,7 @@ Read the following resume carefully and answer the specific questions regarding ``` ## Curriculum -I am a software engineer with 3 years of experience in Swift and Python. I have worked on projects including an i work 2 years with MQTT protocol. +I had a degree in computer science. I have worked 2 years with MQTT protocol. ## Question @@ -314,6 +314,20 @@ How many years of experience do you have with IoT? ## Answer +2 +``` +## Example 1 +``` +## Curriculum + +I had a degree in computer science. + +## Question + +How many years of experience do you have with Bash? + +## Answer + 2 ``` From a6d1d9fe42f4702bc9444b952260b598f0169cf7 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 19:24:14 +0100 Subject: [PATCH 18/23] save old answer for not use too much money --- linkedIn_easy_applier.py | 87 ++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/linkedIn_easy_applier.py b/linkedIn_easy_applier.py index f51b578..48f8bf4 100644 --- a/linkedIn_easy_applier.py +++ b/linkedIn_easy_applier.py @@ -28,27 +28,23 @@ class LinkedInEasyApplier: self.set_old_answers = set_old_answers self.gpt_answerer = gpt_answerer self.resume_generator_manager = resume_generator_manager - self.questions_data = [] + self.all_data = self._load_questions_from_json() def _load_questions_from_json(self) -> List[dict]: output_file = 'answers.json' try: - # Leggi i dati esistenti dal file try: with open(output_file, 'r') as f: try: - all_data = json.load(f) - if not isinstance(all_data, list): + data = json.load(f) + if not isinstance(data, list): raise ValueError("JSON file format is incorrect. Expected a list of questions.") except json.JSONDecodeError: - # Se il file è vuoto o non contiene JSON valido, inizializza come lista vuota - all_data = [] + data = [] except FileNotFoundError: - # Se il file non esiste, inizializza come lista vuota - all_data = [] - - return all_data + data = [] + return data except Exception: tb_str = traceback.format_exc() raise Exception(f"Error loading questions data from JSON file: \nTraceback:\n{tb_str}") @@ -245,13 +241,22 @@ class LinkedInEasyApplier: if radios: question_text = section.text.lower() options = [radio.text.lower() for radio in radios] + + existing_answer = None + for item in self.all_data: + if self._sanitize_text(question_text) in item['question'] and item['type'] == 'radio': + existing_answer = item + break + if existing_answer: + self._select_radio(radios, existing_answer['answer']) + return True + answer = self.gpt_answerer.answer_question_from_options(question_text, options) - self._select_radio(radios, answer) self._save_questions_to_json({'type': 'radio', 'question': question_text, 'answer': answer}) + self._select_radio(radios, answer) return True return False - def _find_and_handle_textbox_question(self, section: WebElement) -> bool: text_fields = section.find_elements(By.TAG_NAME, 'input') + section.find_elements(By.TAG_NAME, 'textarea') if text_fields: @@ -259,13 +264,23 @@ class LinkedInEasyApplier: question_text = section.find_element(By.TAG_NAME, 'label').text.lower() is_numeric = self._is_numeric_field(text_field) if is_numeric: - answer = self.gpt_answerer.answer_question_numeric(question_text) question_type = 'numeric' + answer = self.gpt_answerer.answer_question_numeric(question_text) else: - answer = self.gpt_answerer.answer_question_textual_wide_range(question_text) question_type = 'textbox' - self._enter_text(text_field, answer) + answer = self.gpt_answerer.answer_question_textual_wide_range(question_text) + + + existing_answer = None + for item in self.all_data: + if item['question'] == self._sanitize_text(question_text) and item['type'] == question_type: + existing_answer = item + break + if existing_answer: + self._enter_text(text_field, existing_answer['answer']) + return True self._save_questions_to_json({'type': question_type, 'question': question_text, 'answer': answer}) + self._enter_text(text_field, answer) return True return False @@ -273,9 +288,22 @@ class LinkedInEasyApplier: date_fields = section.find_elements(By.CLASS_NAME, 'artdeco-datepicker__input ') if date_fields: date_field = date_fields[0] + question_text = section.text.lower() answer_date = self.gpt_answerer.answer_question_date() - self._enter_text(date_field, answer_date.strftime("%Y-%m-%d")) - self._save_questions_to_json({'type': 'date', 'question': section.text.lower(), 'answer': answer_date.strftime("%Y-%m-%d")}) + answer_text = answer_date.strftime("%Y-%m-%d") + + + existing_answer = None + for item in self.all_data: + if self._sanitize_text(question_text) in item['question'] and item['type'] == 'date': + existing_answer = item + break + if existing_answer: + self._enter_text(date_field, existing_answer['answer']) + return True + + self._save_questions_to_json({'type': 'date', 'question': question_text, 'answer': answer_text}) + self._enter_text(date_field, answer_text) return True return False @@ -287,9 +315,19 @@ class LinkedInEasyApplier: if dropdown: select = Select(dropdown) options = [option.text for option in select.options] + + existing_answer = None + for item in self.all_data: + if self._sanitize_text(question_text) in item['question'] and item['type'] == 'dropdown': + existing_answer = item + break + if existing_answer: + self._select_dropdown_option(dropdown, existing_answer['answer']) + return True + answer = self.gpt_answerer.answer_question_from_options(question_text, options) - self._select_dropdown_option(dropdown, answer) self._save_questions_to_json({'type': 'dropdown', 'question': question_text, 'answer': answer}) + self._select_dropdown_option(dropdown, answer) return True except Exception: return False @@ -323,22 +361,21 @@ class LinkedInEasyApplier: try: with open(output_file, 'r') as f: try: - all_data = json.load(f) - if not isinstance(all_data, list): + data = json.load(f) + if not isinstance(data, list): raise ValueError("JSON file format is incorrect. Expected a list of questions.") except json.JSONDecodeError: - all_data = [] + data = [] except FileNotFoundError: - all_data = [] - all_data.append(question_data) + data = [] + data.append(question_data) with open(output_file, 'w') as f: - json.dump(all_data, f, indent=4) + json.dump(data, f, indent=4) except Exception: tb_str = traceback.format_exc() raise Exception(f"Error saving questions data to JSON file: \nTraceback:\n{tb_str}") - def _sanitize_text(self, text: str) -> str: sanitized_text = text.lower() sanitized_text = sanitized_text.strip() From 27100f3455e0351e67261f8b989b592a8ed3ccf3 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 19:31:17 +0100 Subject: [PATCH 19/23] better --- strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings.py b/strings.py index 557ab5b..242d198 100644 --- a/strings.py +++ b/strings.py @@ -295,7 +295,7 @@ Read the following resume carefully and answer the specific questions regarding 3. **Inference Over Default Response:** Always strive to infer experience based on the available information. If direct experience cannot be confirmed, use related skills, projects, and academic background to estimate a plausible number of years. Avoid defaulting to 0 if you can infer any relevant experience. 4. **Handling Experience Estimates:** - - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Aim to keep the values as high as possible and avoid using "0" as a response unless absolutely necessary. + - **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Aim to keep the values as high as possible avoid using "0" as a response unless absolutely necessary. - **For High Experience:** For high levels of experience, ensure the number provided is as certain as possible and based on clear evidence from the resume. Avoid making inferences for high experience levels unless the evidence is strong. From 06b176aa37a7bf4e118319b2ec46a53071e85291 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 20:02:45 +0100 Subject: [PATCH 20/23] refactro --- .gitignore | 3 ++- main.py | 12 ++++++------ gpt.py => src/gpt.py | 2 +- .../job_application_profile.py | 0 .../linkedIn_authenticator.py | 0 linkedIn_bot_facade.py => src/linkedIn_bot_facade.py | 0 .../linkedIn_easy_applier.py | 2 +- .../linkedIn_job_manager.py | 4 ++-- strings.py => src/strings.py | 0 utils.py => src/utils.py | 10 +++------- 10 files changed, 15 insertions(+), 18 deletions(-) rename gpt.py => src/gpt.py (99%) rename job_application_profile.py => src/job_application_profile.py (100%) rename linkedIn_authenticator.py => src/linkedIn_authenticator.py (100%) rename linkedIn_bot_facade.py => src/linkedIn_bot_facade.py (100%) rename linkedIn_easy_applier.py => src/linkedIn_easy_applier.py (99%) rename linkedIn_job_manager.py => src/linkedIn_job_manager.py (99%) rename strings.py => src/strings.py (100%) rename utils.py => src/utils.py (91%) diff --git a/.gitignore b/.gitignore index 2d34995..21ead07 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ generated_cv* resume.html .vscode chrome_profile -lib* \ No newline at end of file +lib* +answers.json diff --git a/main.py b/main.py index 2028866..9685677 100644 --- a/main.py +++ b/main.py @@ -9,12 +9,12 @@ from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager from selenium.common.exceptions import WebDriverException, TimeoutException 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 job_application_profile import JobApplicationProfile +from src.utils import chromeBrowserOptions +from src.gpt import GPTAnswerer +from src.linkedIn_authenticator import LinkedInAuthenticator +from src.linkedIn_bot_facade import LinkedInBotFacade +from src.linkedIn_job_manager import LinkedInJobManager +from src.job_application_profile import JobApplicationProfile # Suppress stderr sys.stderr = open(os.devnull, 'w') diff --git a/gpt.py b/src/gpt.py similarity index 99% rename from gpt.py rename to src/gpt.py index 368572c..5794cca 100644 --- a/gpt.py +++ b/src/gpt.py @@ -13,7 +13,7 @@ from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from Levenshtein import distance -import strings +import src.strings as strings load_dotenv() diff --git a/job_application_profile.py b/src/job_application_profile.py similarity index 100% rename from job_application_profile.py rename to src/job_application_profile.py diff --git a/linkedIn_authenticator.py b/src/linkedIn_authenticator.py similarity index 100% rename from linkedIn_authenticator.py rename to src/linkedIn_authenticator.py diff --git a/linkedIn_bot_facade.py b/src/linkedIn_bot_facade.py similarity index 100% rename from linkedIn_bot_facade.py rename to src/linkedIn_bot_facade.py diff --git a/linkedIn_easy_applier.py b/src/linkedIn_easy_applier.py similarity index 99% rename from linkedIn_easy_applier.py rename to src/linkedIn_easy_applier.py index 48f8bf4..0243681 100644 --- a/linkedIn_easy_applier.py +++ b/src/linkedIn_easy_applier.py @@ -17,7 +17,7 @@ from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import Select, WebDriverWait from selenium.webdriver import ActionChains -import utils +import src.utils as utils class LinkedInEasyApplier: def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any, resume_generator_manager): diff --git a/linkedIn_job_manager.py b/src/linkedIn_job_manager.py similarity index 99% rename from linkedIn_job_manager.py rename to src/linkedIn_job_manager.py index e6f2940..6190df0 100644 --- a/linkedIn_job_manager.py +++ b/src/linkedIn_job_manager.py @@ -6,9 +6,9 @@ from itertools import product from pathlib import Path from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By -import utils +import src.utils as utils from job import Job -from linkedIn_easy_applier import LinkedInEasyApplier +from src.linkedIn_easy_applier import LinkedInEasyApplier import json diff --git a/strings.py b/src/strings.py similarity index 100% rename from strings.py rename to src/strings.py diff --git a/utils.py b/src/utils.py similarity index 91% rename from utils.py rename to src/utils.py index bc5cbea..ea7c07b 100644 --- a/utils.py +++ b/src/utils.py @@ -50,7 +50,7 @@ def scroll_slow(driver, scrollable_element, start=0, end=3600, step=100, reverse def chromeBrowserOptions(): ensure_chrome_profile() options = webdriver.ChromeOptions() - """options.add_argument("--start-maximized") # Avvia il browser a schermo intero + options.add_argument("--start-maximized") # Avvia il browser a schermo intero options.add_argument("--no-sandbox") # Disabilita la sandboxing per migliorare le prestazioni options.add_argument("--disable-dev-shm-usage") # Utilizza una directory temporanea per la memoria condivisa options.add_argument("--ignore-certificate-errors") # Ignora gli errori dei certificati SSL @@ -63,7 +63,6 @@ def chromeBrowserOptions(): options.add_argument("--disable-popup-blocking") # Disabilita il blocco dei popup options.add_argument("--no-first-run") # Disabilita la configurazione iniziale del browser options.add_argument("--no-default-browser-check") # Disabilita il controllo del browser predefinito - options.add_argument("--single-process") # Esegui Chrome in un solo processo options.add_argument("--disable-logging") # Disabilita il logging options.add_argument("--disable-autofill") # Disabilita l'autocompletamento dei moduli options.add_argument("--disable-plugins") # Disabilita i plugin del browser @@ -84,11 +83,8 @@ def chromeBrowserOptions(): options.add_argument('--user-data-dir=' + initialPath) options.add_argument("--profile-directory=" + profileDir) else: - options.add_argument("--incognito")""" - initialPath = os.path.dirname(chromeProfilePath) - profileDir = os.path.basename(chromeProfilePath) - options.add_argument('--user-data-dir=' + initialPath) - options.add_argument("--profile-directory=" + profileDir) + options.add_argument("--incognito") + return options From b85a3c4719c0c46308b8ddf85946ebf3b3d08076 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 21:16:01 +0100 Subject: [PATCH 21/23] correct lib --- requirements.txt | Bin 688 -> 670 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index bbe9375cee953eaa266eddd77baf945f985762b8..f74a689c00362ab6724bec113a729791b031cce0 100644 GIT binary patch delta 7 OcmdnMI*)b3JSG4OZ33wP delta 26 hcmbQox`B1WJSHIrhBAg!h9ZVyhD?ThhCBu%1^{AY1~UKv From 08fa73c8de39ad36380c1bb9fcd49121fd549101 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 22:20:48 +0100 Subject: [PATCH 22/23] final --- .gitignore | 3 - README.md | 317 ++++++++++++++++++--- data_folder/config.yaml | 50 ++-- data_folder/plain_text_resume.yaml | 180 +++++------- data_folder/secrets.yaml | 6 +- data_folder_example/config.yaml | 39 +++ data_folder_example/plain_text_resume.yaml | 133 +++++++++ data_folder_example/secrets.yaml | 3 + job.py | 5 + src/linkedIn_easy_applier.py | 19 +- src/linkedIn_job_manager.py | 1 + 11 files changed, 570 insertions(+), 186 deletions(-) create mode 100644 data_folder_example/config.yaml create mode 100644 data_folder_example/plain_text_resume.yaml create mode 100644 data_folder_example/secrets.yaml diff --git a/.gitignore b/.gitignore index 21ead07..4e73720 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,8 @@ test* openaiSelenium* open_ai_calls.json _* -data_folder* .venv generated_cv* -resume.html .vscode chrome_profile -lib* answers.json diff --git a/README.md b/README.md index 05ac37e..4004be7 100644 --- a/README.md +++ b/README.md @@ -175,55 +175,281 @@ This file contains your resume information in a structured format. Fill it out w Each section has specific fields to fill out: -- `personal_information:` - - Contains basic personal details - - Example: `name: "John Doe"` +#### Personal Information -- `self_identification:` - - Optional demographic information - - Example: `gender: "Male"` +##### Description +This section contains basic personal details to identify yourself and provide contact information. -- `legal_authorization:` - - Work authorization status - - Use `true` or `false` for each field - - Example: `usWorkAuthorization: true` +- **name**: Your first name. +- **surname**: Your last name or family name. +- **date_of_birth**: Your birth date in the format DD/MM/YYYY. +- **country**: The country where you currently reside. +- **city**: The city where you currently live. +- **address**: Your full address, including street and number. +- **phone_prefix**: The international dialing code for your phone number (e.g., +1 for the USA, +44 for the UK). +- **phone**: Your phone number without the international prefix. +- **email**: Your primary email address. +- **github**: URL to your GitHub profile, if applicable. +- **linkedin**: URL to your LinkedIn profile, if applicable. -- `work_preferences:` - - Your work-related preferences - - Use `true` or `false` for each field - - Example: `remoteWork: true` +##### Example +```yaml +personal_information: + name: "Jane" + surname: "Doe" + date_of_birth: "01/01/1990" + country: "USA" + city: "New York" + address: "123 Main St" + phone_prefix: "+1" + phone: "5551234567" + email: "jane.doe@example.com" + github: "https://github.com/janedoe" + linkedin: "https://www.linkedin.com/in/janedoe/" +``` -- `education_details:` - - List your educational background - - Include degree, university, GPA, graduation year, field of study, and skills acquired - - Example: - ```yaml - - degree: "Bachelor's" - university: "University of Example" - gpa: "3.8" - graduationYear: "2022" - fieldOfStudy: "Computer Science" - skillsAcquired: - problemSolving: "4" - ``` +#### Education Details -- `experience_details:` - - List your work experiences - - Include position, company, employment period, location, industry, key responsibilities, and skills acquired - - Example: - ```yaml - - position: "Software Developer" - company: "Tech Corp" - employmentPeriod: "Jan 2020 - Present" - location: "San Francisco, USA" - industry: "Technology" - keyResponsibilities: - responsibility1: "Developed web applications using React" - skillsAcquired: - adaptability: "3" - ``` +##### Description +This section outlines your academic background, including degrees earned and relevant coursework. -- Other sections like `projects`, `availability`, `salary_expectations`, `certifications`, `skills`, `languages`, and `interests` follow a similar format, with each item on a new line. +- **degree**: The type of degree obtained (e.g., Bachelor's Degree, Master's Degree). +- **university**: The name of the university or institution where you studied. +- **gpa**: Your Grade Point Average or equivalent measure of academic performance. +- **graduation_year**: The year you graduated. +- **field_of_study**: The major or focus area of your studies. +- **exam**: A list of courses or subjects taken along with their respective grades. + +##### Example +```yaml +education_details: + - degree: "Bachelor's Degree" + university: "University of Example" + gpa: "3.8/4" + graduation_year: "2022" + field_of_study: "Software Engineering" + exam: + Algorithms: "A" + Data Structures: "B+" + Database Systems: "A" + Operating Systems: "A-" + Web Development: "B" +``` + +#### Experience Details + +##### Description +This section details your work experience, including job roles, companies, and key responsibilities. + +- **position**: Your job title or role. +- **company**: The name of the company or organization where you worked. +- **employment_period**: The timeframe during which you were employed in the role (e.g., MM/YYYY - MM/YYYY). +- **location**: The city and country where the company is located. +- **industry**: The industry or field in which the company operates. +- **key_responsibilities**: A list of major responsibilities or duties you had in the role. +- **skills_acquired**: Skills or expertise gained through this role. + +##### Example +```yaml +experience_details: + - position: "Software Developer" + company: "Tech Innovations Inc." + employment_period: "06/2021 - Present" + location: "San Francisco, CA" + industry: "Technology" + key_responsibilities: + - responsibility_1: "Developed web applications using React and Node.js" + - responsibility_2: "Collaborated with cross-functional teams to design and implement new features" + - responsibility_3: "Troubleshot and resolved complex software issues" + skills_acquired: + - "React" + - "Node.js" + - "Software Troubleshooting" +``` + +#### Projects + +##### Description +Include notable projects you have worked on, including personal or professional projects. + +- **name**: The name or title of the project. +- **description**: A brief summary of what the project involves or its purpose. +- **link**: URL to the project, if available (e.g., GitHub repository, website). + +##### Example +```yaml +projects: + - name: "Weather App" + description: "A web application that provides real-time weather information using a third-party API." + link: "https://github.com/janedoe/weather-app" + - name: "Task Manager" + description: "A task management tool with features for tracking and prioritizing tasks." + link: "https://github.com/janedoe/task-manager" +``` + +#### Achievements + +##### Description +Highlight notable accomplishments or awards you have received. + +- **name**: The title or name of the achievement. +- **description**: A brief explanation of the achievement and its significance. + +##### Example +```yaml +achievements: + - name: "Employee of the Month" + description: "Recognized for exceptional performance and contributions to the team." + - name: "Hackathon Winner" + description: "Won first place in a national hackathon competition." +``` + +#### Certifications + +##### Description +Include any professional certifications you have earned. + +- **certification_name**: The name of the certification. + +##### Example +```yaml +certifications: + - "Certified Scrum Master" + - "AWS Certified Solutions Architect" +``` + +#### Languages + +##### Description +Detail the languages you speak and your proficiency level in each. + +- **language**: The name of the language. +- **proficiency**: Your level of proficiency (e.g., Native, Fluent, Intermediate). + +##### Example +```yaml +languages: + - language: "English" + proficiency: "Fluent" + - language: "Spanish" + proficiency: "Intermediate" +``` + +#### Interests + +##### Description +Mention your professional or personal interests that may be relevant to your career. + +- **interest**: A list of interests or hobbies. + +##### Example +```yaml +interests: + - "Machine Learning" + - "Cybersecurity" + - "Open Source Projects" + - "Digital Marketing" + - "Entrepreneurship" +``` + +#### Availability + +##### Description +State your current availability or notice period. + +- **notice_period**: The amount of time required before you can start a new role (e.g., "2 weeks", "1 month"). + +##### Example +```yaml +availability: + notice_period: "2 weeks" +``` + +#### Salary Expectations + +##### Description +Provide your expected salary range. + +- **salary_range_usd**: The salary range you are expecting, expressed in USD. + +##### Example +```yaml +salary_expectations: + salary_range_usd: "80000 - 100000" +``` + +#### Self-Identification + +##### Description +Provide information related to personal identity, including gender and pronouns. + +- **gender**: Your gender identity. +- **pronouns**: The pronouns you use (e.g., He/Him, She/Her, They/Them). +- **veteran**: Your status as a veteran (e.g., Yes, No). +- **disability**: Whether you have a disability (e.g., Yes, No). +- **ethnicity**: Your ethnicity. + +##### Example +```yaml +self_identification: + gender: "Female" + pronouns: "She/Her" + veteran: "No" + disability: "No" + ethnicity: "Asian" +``` + +#### Legal Authorization + +##### Description +Indicate your legal ability to work in various locations. + +- **eu_work_authorization**: Whether you are authorized to work in the European Union (Yes/No). +- **us_work_authorization**: Whether you are authorized to work in the United States (Yes/No). +- **requires_us_visa**: Whether you require a visa to work in the US (Yes/No). +- **requires_us_sponsorship**: Whether you require sponsorship to work in the US (Yes/No). +- **requires_eu_visa**: Whether you require a visa to work in the EU (Yes/No). +- **legally_allowed_to_work_in_eu**: Whether you are legally allowed to work in the EU (Yes/No). +- **legally_allowed_to_work_in_us**: Whether you are legally allowed to work in the US (Yes/No). +- **requires_eu_sponsorship**: Whether you require sponsorship to work in the EU (Yes/No). + +##### Example +```yaml +legal_authorization: + eu_work_authorization: "Yes" + us_work_authorization: "No" + requires_us_visa: "Yes" + requires_us_sponsorship: "Yes" + requires_eu_visa: "No" + legally_allowed_to_work_in_eu: "Yes" + legally_allowed_to_work_in_us: "No" + requires_eu_sponsorship: "No" +``` + +#### Work Preferences + +##### Description +Specify your preferences for work arrangements and conditions. + +- **remote_work**: Whether you are open to remote work (Yes/No). +- **in_person_work**: Whether you are open to in-person work (Yes/No). +- **open_to_relocation**: Whether you are willing to relocate for a job (Yes/No). +- **willing_to_complete_assessments**: Whether you are willing to complete job assessments (Yes/No). +- **willing_to_undergo_drug_tests**: Whether you are willing to undergo drug testing (Yes/No). +- **willing_to_undergo_background_checks**: Whether you are willing to undergo background checks (Yes/No). + +##### Example +```yaml +work + +_preferences: + remote_work: "Yes" + in_person_work: "No" + open_to_relocation: "Yes" + willing_to_complete_assessments: "Yes" + willing_to_undergo_drug_tests: "No" + willing_to_undergo_background_checks: "Yes" +``` ### PLUS. data_folder_example @@ -247,7 +473,6 @@ Using this folder as a guide can be particularly helpful for: 2. Seeing examples of valid data for each field 3. Having a reference point while filling out your personal files -#### Important Note ## Usage 0. **LinkedIn language** @@ -276,7 +501,7 @@ Using this folder as a guide can be particularly helpful for: ## Documentation -For detailed information on each component and their respective roles, please refer to the [Documentation](documentation.md) file. +TODO ): ## Troubleshooting @@ -292,7 +517,7 @@ LinkedIn_AIHawk provides a significant advantage in the modern job market by aut ## Contributors -- [feder-cr](https://github.com/feder-cr/) - Creator and Maintainer +- [feder-cr](https://github.com/feder-cr) - Creator and Lead Developer LinkedIn_AIHawk is still in beta, and your feedback, suggestions, and contributions are highly valued. Feel free to open issues, suggest enhancements, or submit pull requests to help improve the project. Let's work together to make LinkedIn_AIHawk an even more powerful tool for job seekers worldwide. diff --git a/data_folder/config.yaml b/data_folder/config.yaml index ca23ac6..58a6f1c 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -1,40 +1,42 @@ -remote: true +remote: [true/false] experienceLevel: - internship: true - entry: false - associate: true - mid-senior level: false - director: true - executive: false + internship: [true/false] + entry: [true/false] + associate: [true/false] + mid-senior level: [true/false] + director: [true/false] + executive: [true/false] jobTypes: - full-time: true - contract: false - part-time: false - temporary: true - internship: false - other: false - volunteer: true + full-time: [true/false] + contract: [true/false] + part-time: [true/false] + temporary: [true/false] + internship: [true/false] + other: [true/false] + volunteer: [true/false] date: - all time: false - month: true - week: false - 24 hours: true + all time: [true/false] + month: [true/false] + week: [true/false] + 24 hours: [true/false] positions: - - Software Enginner ML - + - position1 + - position2 locations: - - USA + - Country1 + - Country2 distance: 100 companyBlacklist: - - Noir - - Crossover + - Company1 + - Company2 titleBlacklist: - - diocane + - word1 + - word2 \ No newline at end of file diff --git a/data_folder/plain_text_resume.yaml b/data_folder/plain_text_resume.yaml index c0ad978..aabcee4 100644 --- a/data_folder/plain_text_resume.yaml +++ b/data_folder/plain_text_resume.yaml @@ -1,133 +1,101 @@ personal_information: - name: "Liam" - surname: "Murphy" - date_of_birth: "15/08/1995" - country: "Ireland" - city: "Galway" - address: "Galway City Center" - phone_prefix: "+353" - phone: "871234567" - email: "liam.murphy@gmail.com" - github: "https://github.com/liam-murphy" - linkedin: "https://www.linkedin.com/in/liam-murphy/" - + name: "[Your Name]" + surname: "[Your Surname]" + date_of_birth: "[DD/MM/YYYY]" + country: "[Your Country]" + city: "[Your City]" + address: "[Your Address]" + phone_prefix: "[Your Phone Prefix]" + phone: "[Your Phone Number]" + email: "[Your Email Address]" + github: "[Your GitHub Profile URL]" + linkedin: "[Your LinkedIn Profile URL]" + education_details: - - degree: "Bachelor's Degree" - university: "National University of Ireland, Galway" - gpa: "4/4" - graduation_year: "2020" - field_of_study: "Computer Science" + - degree: "[Your Degree]" + university: "[Your University]" + gpa: "[Your GPA]" + graduation_year: "[Year of Graduation]" + field_of_study: "[Your Field of Study]" exam: - Information Theory and Inference: "4" - Algorithm Analysis and Design: "4" - Object-Oriented Languages and Programming: "4" - Linear Algebra and Numerical Analysis: "4" - Database: "4" + [Course Name 1]: "[Grade]" + [Course Name 2]: "[Grade]" + [Course Name 3]: "[Grade]" + [Course Name 4]: "[Grade]" + [Course Name 5]: "[Grade]" experience_details: - - position: "Co-Founder & Software Engineer" - company: "CryptoWave Solutions" - employment_period: "03/2021 - Present" - location: "Ireland" - industry: "Blockchain Technology" + - position: "[Your Job Title]" + company: "[Company Name]" + employment_period: "[Start Date] - [End Date]" + location: "[Location]" + industry: "[Industry]" key_responsibilities: - - responsibility_1: "Co-founded and led a startup specializing in app and software development with a focus on blockchain technology" - - responsibility_2: "Provided blockchain consultations for 10+ companies, enhancing their software capabilities with secure, decentralized solutions" - - responsibility_3: "Developed blockchain applications, integrated cutting-edge technology to meet client needs and drive industry innovation" + - responsibility_1: "[Key Responsibility 1]" + - responsibility_2: "[Key Responsibility 2]" + - responsibility_3: "[Key Responsibility 3]" skills_acquired: - - "Blockchain development" - - "Software engineering" - - "Consultancy" - - - position: "Research Intern" - company: "National University of Ireland, Galway" - employment_period: "11/2022 - 03/2023" - location: "Galway, Ireland" - industry: "IoT Security Research" - key_responsibilities: - - responsibility_1: "Conducted in-depth research on IoT security, focusing on binary instrumentation and runtime monitoring" - - responsibility_2: "Performed in-depth study of the MQTT protocol and Falco" - - responsibility_3: "Developed multiple software components including MQTT packet analysis library, Falco adapter, and RML monitor in Prolog" - - responsibility_4: "Authored thesis 'Binary Instrumentation for Runtime Monitoring of Internet of Things Systems Using Falco'" - skills_acquired: - - "IoT security" - - "Binary instrumentation" - - "MQTT protocol" - - "Prolog programming" - - - position: "Software Engineer" - company: "University Hospital Galway" - employment_period: "05/2022 - 11/2022" - location: "Galway, Ireland" - industry: "Healthcare IT" - key_responsibilities: - - responsibility_1: "Integrated and enforced robust security protocols" - - responsibility_2: "Developed and maintained a critical software tool for password validation used by over 1,600 employees" - - responsibility_3: "Played an integral role in the hospital's cybersecurity team" - skills_acquired: - - "Cybersecurity" - - "Software development" - - "Password validation" + - "[Skill 1]" + - "[Skill 2]" + - "[Skill 3]" projects: - - name: "JobBot" - description: "AI-driven tool to automate and personalize job applications on LinkedIn, gained over 3000 stars on GitHub, improving efficiency and reducing application time" - link: "https://github.com/liam-murphy/jobbot" - - name: "mqtt-packet-parser" - description: "Developed a Node.js module for parsing MQTT packets, improved parsing efficiency by 40%" - link: "https://github.com/liam-murphy/mqtt-packet-parser" + - name: "[Project Name]" + description: "[Brief Description of the Project]" + link: "[Project URL]" + - name: "[Project Name]" + description: "[Brief Description of the Project]" + link: "[Project URL]" achievements: - - name: "Winner of an Irish public competition" - description: "Won first place in a public competition with a perfect score of 70/70, securing a Software Developer position at University Hospital Galway" - - name: "Galway Merit Scholarship" - description: "Awarded annually from 2018 to 2020 in recognition of academic excellence and contribution" - - name: "GitHub Recognition" - description: "Gained over 3000 stars on GitHub with JobBot project" + - name: "[Achievement Title]" + description: "[Brief Description of the Achievement]" + - name: "[Achievement Title]" + description: "[Brief Description of the Achievement]" certifications: - - "C1" + - "[Certification Name]" languages: - - language: "English" - proficiency: "Native" - - language: "Spanish" - proficiency: "Professional" + - language: "[Language Name]" + proficiency: "[Proficiency Level]" + - language: "[Language Name]" + proficiency: "[Proficiency Level]" interests: - - "Full-Stack Development" - - "Software Architecture" - - "IoT system design and development" - - "Artificial Intelligence" - - "Cloud Technologies" + - "[Interest 1]" + - "[Interest 2]" + - "[Interest 3]" + - "[Interest 4]" + - "[Interest 5]" availability: - notice_period: "immediately" + notice_period: "[Notice Period]" salary_expectations: - salary_range_usd: "100000" + salary_range_usd: "[Expected Salary Range in USD]" self_identification: - gender: "Male" - pronouns: "He" - veteran: "No" - disability: "No" - ethnicity: "white" + gender: "[Gender]" + pronouns: "[Pronouns]" + veteran: "[Veteran Status]" + disability: "[Disability Status]" + ethnicity: "[Ethnicity]" legal_authorization: - eu_work_authorization: "Yes" - us_work_authorization: "No" - requires_us_visa: "Yes" - requires_us_sponsorship: "Yes" - requires_eu_visa: "No" - legally_allowed_to_work_in_eu: "Yes" - legally_allowed_to_work_in_us: "No" - requires_eu_sponsorship: "No" + eu_work_authorization: "[Yes/No]" + us_work_authorization: "[Yes/No]" + requires_us_visa: "[Yes/No]" + requires_us_sponsorship: "[Yes/No]" + requires_eu_visa: "[Yes/No]" + legally_allowed_to_work_in_eu: "[Yes/No]" + legally_allowed_to_work_in_us: "[Yes/No]" + requires_eu_sponsorship: "[Yes/No]" work_preferences: - remote_work: "Yes" - in_person_work: "Yes" - open_to_relocation: "Yes" - willing_to_complete_assessments: "Yes" - willing_to_undergo_drug_tests: "Yes" - willing_to_undergo_background_checks: "Yes" + remote_work: "[Yes/No]" + in_person_work: "[Yes/No]" + open_to_relocation: "[Yes/No]" + willing_to_complete_assessments: "[Yes/No]" + willing_to_undergo_drug_tests: "[Yes/No]" + willing_to_undergo_background_checks: "[Yes/No]" diff --git a/data_folder/secrets.yaml b/data_folder/secrets.yaml index b92541b..ad24cd8 100644 --- a/data_folder/secrets.yaml +++ b/data_folder/secrets.yaml @@ -1,3 +1,3 @@ -email: [Your Linkedin email] -password: [Your Linkedin password] -openai_api_key: [OpenAi API key, tutorial -> https://medium.com/@lorenzozar/how-to-get-your-own-openai-api-key-f4d44e60c327] \ No newline at end of file +email: myemaillinkedin@gmail.com +password: ImpossiblePassowrd10 +openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR \ No newline at end of file diff --git a/data_folder_example/config.yaml b/data_folder_example/config.yaml new file mode 100644 index 0000000..6e362be --- /dev/null +++ b/data_folder_example/config.yaml @@ -0,0 +1,39 @@ +remote: true + +experienceLevel: + internship: true + entry: true + associate: true + mid-senior level: true + director: false + executive: false + +jobTypes: + full-time: true + contract: false + part-time: false + temporary: true + internship: false + other: false + volunteer: true + +date: + all time: false + month: true + week: false + 24 hours: true + +positions: + - Software Tester + + +locations: + - USA + +distance: 100 + +companyBlacklist: + - Noir + - Crossover + +titleBlacklist: diff --git a/data_folder_example/plain_text_resume.yaml b/data_folder_example/plain_text_resume.yaml new file mode 100644 index 0000000..2ba5666 --- /dev/null +++ b/data_folder_example/plain_text_resume.yaml @@ -0,0 +1,133 @@ +personal_information: + name: "Liam" + surname: "Murphy" + date_of_birth: "15/08/1995" + country: "Ireland" + city: "Galway" + address: "Galway City Center" + phone_prefix: "+353" + phone: "871234567" + email: "liam.murphy@gmail.com" + github: "https://github.com/liam-murphy" + linkedin: "https://www.linkedin.com/in/liam-murphy/" + +education_details: + - degree: "Bachelor's Degree" + university: "National University of Ireland, Galway" + gpa: "4/4" + graduation_year: "2020" + field_of_study: "Computer Science" + exam: + Information Theory and Inference: "4" + Algorithm Analysis and Design: "4" + Object-Oriented Languages and Programming: "4" + Linear Algebra and Numerical Analysis: "4" + Database: "4" + +experience_details: + - position: "Co-Founder & Software Engineer" + company: "CryptoWave Solutions" + employment_period: "03/2021 - Present" + location: "Ireland" + industry: "Blockchain Technology" + key_responsibilities: + - responsibility_1: "Co-founded and led a startup specializing in app and software development with a focus on blockchain technology" + - responsibility_2: "Provided blockchain consultations for 10+ companies, enhancing their software capabilities with secure, decentralized solutions" + - responsibility_3: "Developed blockchain applications, integrated cutting-edge technology to meet client needs and drive industry innovation" + skills_acquired: + - "Blockchain development" + - "Software engineering" + - "Consultancy" + + - position: "Research Intern" + company: "National University of Ireland, Galway" + employment_period: "11/2022 - 03/2023" + location: "Galway, Ireland" + industry: "IoT Security Research" + key_responsibilities: + - responsibility_1: "Conducted in-depth research on IoT security, focusing on binary instrumentation and runtime monitoring" + - responsibility_2: "Performed in-depth study of the MQTT protocol and Falco" + - responsibility_3: "Developed multiple software components including MQTT packet analysis library, Falco adapter, and RML monitor in Prolog" + - responsibility_4: "Authored thesis 'Binary Instrumentation for Runtime Monitoring of Internet of Things Systems Using Falco'" + skills_acquired: + - "IoT security" + - "Binary instrumentation" + - "MQTT protocol" + - "Prolog programming" + + - position: "Software Engineer" + company: "University Hospital Galway" + employment_period: "05/2022 - 11/2022" + location: "Galway, Ireland" + industry: "Healthcare IT" + key_responsibilities: + - responsibility_1: "Integrated and enforced robust security protocols" + - responsibility_2: "Developed and maintained a critical software tool for password validation used by over 1,600 employees" + - responsibility_3: "Played an integral role in the hospital's cybersecurity team" + skills_acquired: + - "Cybersecurity" + - "Software development" + - "Password validation" + +projects: + - name: "JobBot" + description: "AI-driven tool to automate and personalize job applications on LinkedIn, gained over 3000 stars on GitHub, improving efficiency and reducing application time" + link: "https://github.com/liam-murphy/jobbot" + - name: "mqtt-packet-parser" + description: "Developed a Node.js module for parsing MQTT packets, improved parsing efficiency by 40%" + link: "https://github.com/liam-murphy/mqtt-packet-parser" + +achievements: + - name: "Winner of an Irish public competition" + description: "Won first place in a public competition with a perfect score of 70/70, securing a Software Developer position at University Hospital Galway" + - name: "Galway Merit Scholarship" + description: "Awarded annually from 2018 to 2020 in recognition of academic excellence and contribution" + - name: "GitHub Recognition" + description: "Gained over 3000 stars on GitHub with JobBot project" + +certifications: + - "C1" + +languages: + - language: "English" + proficiency: "Native" + - language: "Spanish" + proficiency: "Professional" + +interests: + - "Full-Stack Development" + - "Software Architecture" + - "IoT system design and development" + - "Artificial Intelligence" + - "Cloud Technologies" + +availability: + notice_period: "immediately" + +salary_expectations: + salary_range_usd: "100000" + +self_identification: + gender: "Male" + pronouns: "He" + veteran: "No" + disability: "No" + ethnicity: "white" + +legal_authorization: + eu_work_authorization: "Yes" + us_work_authorization: "No" + requires_us_visa: "Yes" + requires_us_sponsorship: "Yes" + requires_eu_visa: "No" + legally_allowed_to_work_in_eu: "Yes" + legally_allowed_to_work_in_us: "No" + requires_eu_sponsorship: "No" + +work_preferences: + remote_work: "Yes" + in_person_work: "Yes" + open_to_relocation: "Yes" + willing_to_complete_assessments: "Yes" + willing_to_undergo_drug_tests: "Yes" + willing_to_undergo_background_checks: "Yes" \ No newline at end of file diff --git a/data_folder_example/secrets.yaml b/data_folder_example/secrets.yaml new file mode 100644 index 0000000..ad24cd8 --- /dev/null +++ b/data_folder_example/secrets.yaml @@ -0,0 +1,3 @@ +email: myemaillinkedin@gmail.com +password: ImpossiblePassowrd10 +openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR \ No newline at end of file diff --git a/job.py b/job.py index 9ac1842..31fef22 100644 --- a/job.py +++ b/job.py @@ -10,6 +10,7 @@ class Job: description: str = "" summarize_job_description: str = "" pdf_path: str = "" + recruiter_link: str = "" def set_summarize_job_description(self, summarize_job_description): self.summarize_job_description = summarize_job_description @@ -17,6 +18,9 @@ class Job: def set_job_description(self, description): self.description = description + def set_recruiter_link(self, recruiter_link): + self.recruiter_link = recruiter_link + def formatted_job_information(self): """ Formats the job information as a markdown string. @@ -27,6 +31,7 @@ class Job: - Position: {self.title} - At: {self.company} - Location: {self.location} + - Recruiter Profile: {self.recruiter_link or 'Not available'} ## Description {self.description or 'No description provided.'} diff --git a/src/linkedIn_easy_applier.py b/src/linkedIn_easy_applier.py index 0243681..6a4548c 100644 --- a/src/linkedIn_easy_applier.py +++ b/src/linkedIn_easy_applier.py @@ -55,8 +55,8 @@ class LinkedInEasyApplier: time.sleep(random.uniform(3, 5)) try: easy_apply_button = self._find_easy_apply_button() - job_description = self._get_job_description() - job.set_job_description(job_description) + job.set_job_description(self._get_job_description()) + job.set_recruiter_link(self._get_job_recruiter()) actions = ActionChains(self.driver) actions.move_to_element(easy_apply_button).click().perform() self.gpt_answerer.set_job(job) @@ -107,6 +107,19 @@ class LinkedInEasyApplier: tb_str = traceback.format_exc() raise Exception(f"Error getting Job description: \nTraceback:\n{tb_str}") + + def _get_job_recruiter(self): + try: + hiring_team_section = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.XPATH, '//h2[text()="Meet the hiring team"]')) + ) + recruiter_element = hiring_team_section.find_element(By.XPATH, './/following::a[contains(@href, "linkedin.com/in/")]') + recruiter_link = recruiter_element.get_attribute('href') + return recruiter_link + except Exception as e: + print(f"Errore durante l'estrazione del link del recruiter: {e}") + return "" + def _scroll_page(self) -> None: scrollable_element = self.driver.find_element(By.TAG_NAME, 'html') #utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=False) @@ -269,8 +282,6 @@ class LinkedInEasyApplier: else: question_type = 'textbox' answer = self.gpt_answerer.answer_question_textual_wide_range(question_text) - - existing_answer = None for item in self.all_data: if item['question'] == self._sanitize_text(question_text) and item['type'] == question_type: diff --git a/src/linkedIn_job_manager.py b/src/linkedIn_job_manager.py index 6190df0..7de12a4 100644 --- a/src/linkedIn_job_manager.py +++ b/src/linkedIn_job_manager.py @@ -149,6 +149,7 @@ class LinkedInJobManager: "company": job.company, "job_title": job.title, "link": job.link, + "job_recruiter": job.recruiter_link, "job_location": job.location, "pdf_path": pdf_path } From 038ce108b5ca30ad5b09a0437ee2fc000c4ee512 Mon Sep 17 00:00:00 2001 From: feder-cr Date: Fri, 23 Aug 2024 22:23:44 +0100 Subject: [PATCH 23/23] refactor --- documentation.md | 1 - job.py => src/job.py | 0 2 files changed, 1 deletion(-) delete mode 100644 documentation.md rename job.py => src/job.py (100%) diff --git a/documentation.md b/documentation.md deleted file mode 100644 index a3a8cfc..0000000 --- a/documentation.md +++ /dev/null @@ -1 +0,0 @@ -Hi, I'm working on this one! diff --git a/job.py b/src/job.py similarity index 100% rename from job.py rename to src/job.py