refactro
This commit is contained in:
parent
27100f3455
commit
06b176aa37
10 changed files with 15 additions and 18 deletions
327
src/gpt.py
Normal file
327
src/gpt.py
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import json
|
||||
import os
|
||||
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
|
||||
from langchain_core.prompt_values import StringPromptValue
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_openai import ChatOpenAI
|
||||
from Levenshtein import distance
|
||||
|
||||
import src.strings as strings
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class LLMLogger:
|
||||
|
||||
def __init__(self, llm: ChatOpenAI):
|
||||
self.llm = llm
|
||||
|
||||
@staticmethod
|
||||
def log_request(prompts, parsed_reply: Dict[str, Dict]):
|
||||
calls_log = os.path.join(Path("data_folder/output"), "open_ai_calls.json")
|
||||
if isinstance(prompts, StringPromptValue):
|
||||
prompts = prompts.text
|
||||
elif isinstance(prompts, Dict):
|
||||
# Convert prompts to a dictionary if they are not in the expected format
|
||||
prompts = {
|
||||
f"prompt_{i+1}": prompt.content
|
||||
for i, prompt in enumerate(prompts.messages)
|
||||
}
|
||||
else:
|
||||
prompts = {
|
||||
f"prompt_{i+1}": prompt.content
|
||||
for i, prompt in enumerate(prompts.messages)
|
||||
}
|
||||
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Extract token usage details from the response
|
||||
token_usage = parsed_reply["usage_metadata"]
|
||||
output_tokens = token_usage["output_tokens"]
|
||||
input_tokens = token_usage["input_tokens"]
|
||||
total_tokens = token_usage["total_tokens"]
|
||||
|
||||
# Extract model details from the response
|
||||
model_name = parsed_reply["response_metadata"]["model_name"]
|
||||
prompt_price_per_token = 0.00000015
|
||||
completion_price_per_token = 0.0000006
|
||||
|
||||
# Calculate the total cost of the API call
|
||||
total_cost = (input_tokens * prompt_price_per_token) + (
|
||||
output_tokens * completion_price_per_token
|
||||
)
|
||||
|
||||
# Create a log entry with all relevant information
|
||||
log_entry = {
|
||||
"model": model_name,
|
||||
"time": current_time,
|
||||
"prompts": prompts,
|
||||
"replies": parsed_reply["content"], # Response content
|
||||
"total_tokens": total_tokens,
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
"total_cost": total_cost,
|
||||
}
|
||||
|
||||
# Write the log entry to the log file in JSON format
|
||||
with open(calls_log, "a", encoding="utf-8") as f:
|
||||
json_string = json.dumps(log_entry, ensure_ascii=False, indent=4)
|
||||
f.write(json_string + "\n")
|
||||
|
||||
|
||||
class LoggerChatModel:
|
||||
|
||||
def __init__(self, llm: ChatOpenAI):
|
||||
self.llm = llm
|
||||
|
||||
def __call__(self, messages: List[Dict[str, str]]) -> str:
|
||||
# Call the LLM with the provided messages and log the response.
|
||||
reply = self.llm(messages)
|
||||
parsed_reply = self.parse_llmresult(reply)
|
||||
LLMLogger.log_request(prompts=messages, parsed_reply=parsed_reply)
|
||||
return reply
|
||||
|
||||
def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]:
|
||||
# Parse the LLM result into a structured format.
|
||||
content = llmresult.content
|
||||
response_metadata = llmresult.response_metadata
|
||||
id_ = llmresult.id
|
||||
usage_metadata = llmresult.usage_metadata
|
||||
parsed_result = {
|
||||
"content": content,
|
||||
"response_metadata": {
|
||||
"model_name": response_metadata.get("model_name", ""),
|
||||
"system_fingerprint": response_metadata.get("system_fingerprint", ""),
|
||||
"finish_reason": response_metadata.get("finish_reason", ""),
|
||||
"logprobs": response_metadata.get("logprobs", None),
|
||||
},
|
||||
"id": id_,
|
||||
"usage_metadata": {
|
||||
"input_tokens": usage_metadata.get("input_tokens", 0),
|
||||
"output_tokens": usage_metadata.get("output_tokens", 0),
|
||||
"total_tokens": usage_metadata.get("total_tokens", 0),
|
||||
},
|
||||
}
|
||||
return parsed_result
|
||||
|
||||
|
||||
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)
|
||||
)
|
||||
@property
|
||||
def job_description(self):
|
||||
return self.job.description
|
||||
|
||||
@staticmethod
|
||||
def find_best_match(text: str, options: list[str]) -> str:
|
||||
distances = [
|
||||
(option, distance(text.lower(), option.lower())) for option in options
|
||||
]
|
||||
best_option = min(distances, key=lambda x: x[1])[0]
|
||||
return best_option
|
||||
|
||||
@staticmethod
|
||||
def _remove_placeholders(text: str) -> str:
|
||||
text = text.replace("PLACEHOLDER", "")
|
||||
return text.strip()
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_template_string(template: str) -> str:
|
||||
# Preprocess a template string to remove unnecessary indentation.
|
||||
return textwrap.dedent(template)
|
||||
|
||||
def set_resume(self, resume):
|
||||
self.resume = resume
|
||||
|
||||
def set_job(self, job):
|
||||
self.job = job
|
||||
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
|
||||
)
|
||||
prompt = ChatPromptTemplate.from_template(strings.summarize_prompt_template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"text": text})
|
||||
return output
|
||||
|
||||
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 = {
|
||||
"personal_information": self._create_chain(strings.personal_information_template),
|
||||
"self_identification": self._create_chain(strings.self_identification_template),
|
||||
"legal_authorization": self._create_chain(strings.legal_authorization_template),
|
||||
"work_preferences": self._create_chain(strings.work_preferences_template),
|
||||
"education_details": self._create_chain(strings.education_details_template),
|
||||
"experience_details": self._create_chain(strings.experience_details_template),
|
||||
"projects": self._create_chain(strings.projects_template),
|
||||
"availability": self._create_chain(strings.availability_template),
|
||||
"salary_expectations": self._create_chain(strings.salary_expectations_template),
|
||||
"certifications": self._create_chain(strings.certifications_template),
|
||||
"languages": self._create_chain(strings.languages_template),
|
||||
"interests": self._create_chain(strings.interests_template),
|
||||
"cover_letter": self._create_chain(strings.coverletter_template),
|
||||
}
|
||||
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})
|
||||
return output
|
||||
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 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_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_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:
|
||||
output = default_experience
|
||||
return output
|
||||
|
||||
def extract_number_from_string(self, output_str):
|
||||
numbers = re.findall(r"\d+", output_str)
|
||||
if numbers:
|
||||
return int(numbers[0])
|
||||
else:
|
||||
raise ValueError("No numbers found in the string")
|
||||
|
||||
def answer_question_from_options(self, question: str, options: list[str]) -> str:
|
||||
func_template = self._preprocess_template_string(strings.options_template)
|
||||
prompt = ChatPromptTemplate.from_template(func_template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
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"
|
||||
132
src/job_application_profile.py
Normal file
132
src/job_application_profile.py
Normal file
|
|
@ -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")
|
||||
87
src/linkedIn_authenticator.py
Normal file
87
src/linkedIn_authenticator.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import time
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
class LinkedInAuthenticator:
|
||||
|
||||
def __init__(self, driver=None):
|
||||
self.driver = driver
|
||||
self.email = ""
|
||||
self.password = ""
|
||||
|
||||
def set_secrets(self, email, password):
|
||||
self.email = email
|
||||
self.password = password
|
||||
|
||||
def start(self):
|
||||
print("Starting Chrome browser to log in to LinkedIn.")
|
||||
self.driver.get('https://www.linkedin.com')
|
||||
self.wait_for_page_load()
|
||||
if not self.is_logged_in():
|
||||
self.handle_login()
|
||||
|
||||
def handle_login(self):
|
||||
print("Navigating to the LinkedIn login page...")
|
||||
self.driver.get("https://www.linkedin.com/login")
|
||||
try:
|
||||
self.enter_credentials()
|
||||
self.submit_login_form()
|
||||
except NoSuchElementException:
|
||||
print("Could not log in to LinkedIn. Please check your credentials.")
|
||||
time.sleep(35) #TODO fix better
|
||||
self.handle_security_check()
|
||||
|
||||
def enter_credentials(self):
|
||||
try:
|
||||
email_field = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "username"))
|
||||
)
|
||||
email_field.send_keys(self.email)
|
||||
password_field = self.driver.find_element(By.ID, "password")
|
||||
password_field.send_keys(self.password)
|
||||
except TimeoutException:
|
||||
print("Login form not found. Aborting login.")
|
||||
|
||||
def submit_login_form(self):
|
||||
try:
|
||||
login_button = self.driver.find_element(By.XPATH, '//button[@type="submit"]')
|
||||
login_button.click()
|
||||
except NoSuchElementException:
|
||||
print("Login button not found. Please verify the page structure.")
|
||||
|
||||
def handle_security_check(self):
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.url_contains('https://www.linkedin.com/checkpoint/challengesV2/')
|
||||
)
|
||||
print("Security checkpoint detected. Please complete the challenge.")
|
||||
WebDriverWait(self.driver, 300).until(
|
||||
EC.url_contains('https://www.linkedin.com/feed/')
|
||||
)
|
||||
print("Security check completed")
|
||||
except TimeoutException:
|
||||
print("Security check not completed. Please try again later.")
|
||||
|
||||
def is_logged_in(self):
|
||||
self.driver.get('https://www.linkedin.com/feed')
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, 'share-box-feed-entry__trigger'))
|
||||
)
|
||||
buttons = self.driver.find_elements(By.CLASS_NAME, 'share-box-feed-entry__trigger')
|
||||
if any(button.text.strip() == 'Start a post' for button in buttons):
|
||||
print("User is already logged in.")
|
||||
return True
|
||||
except TimeoutException:
|
||||
pass
|
||||
return False
|
||||
|
||||
def wait_for_page_load(self, timeout=10):
|
||||
try:
|
||||
WebDriverWait(self.driver, timeout).until(
|
||||
lambda d: d.execute_script('return document.readyState') == 'complete'
|
||||
)
|
||||
except TimeoutException:
|
||||
print("Page load timed out.")
|
||||
73
src/linkedIn_bot_facade.py
Normal file
73
src/linkedIn_bot_facade.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
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 = LinkedInBotState()
|
||||
self.job_application_profile = None
|
||||
self.resume = None
|
||||
self.email = None
|
||||
self.password = None
|
||||
self.parameters = None
|
||||
|
||||
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.job_application_profile_set = True
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
self._validate_non_empty(parameters, "Parameters")
|
||||
self.parameters = parameters
|
||||
self.apply_component.set_parameters(parameters)
|
||||
self.state.parameters_set = True
|
||||
|
||||
def start_login(self):
|
||||
self.state.validate_state(['credentials_set'])
|
||||
self.login_component.set_secrets(self.email, self.password)
|
||||
self.login_component.start()
|
||||
self.state.logged_in = True
|
||||
|
||||
def start_apply(self):
|
||||
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.")
|
||||
387
src/linkedIn_easy_applier.py
Normal file
387
src/linkedIn_easy_applier.py
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
from datetime import date
|
||||
from typing import List, Optional, Any, Tuple
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
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
|
||||
from selenium.webdriver import ActionChains
|
||||
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):
|
||||
if resume_dir is None or not os.path.exists(resume_dir):
|
||||
resume_dir = None
|
||||
self.driver = driver
|
||||
self.resume_path = resume_dir
|
||||
self.set_old_answers = set_old_answers
|
||||
self.gpt_answerer = gpt_answerer
|
||||
self.resume_generator_manager = resume_generator_manager
|
||||
self.all_data = self._load_questions_from_json()
|
||||
|
||||
|
||||
def _load_questions_from_json(self) -> List[dict]:
|
||||
output_file = 'answers.json'
|
||||
try:
|
||||
try:
|
||||
with open(output_file, 'r') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, list):
|
||||
raise ValueError("JSON file format is incorrect. Expected a list of questions.")
|
||||
except json.JSONDecodeError:
|
||||
data = []
|
||||
except FileNotFoundError:
|
||||
data = []
|
||||
return 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)
|
||||
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)
|
||||
actions = ActionChains(self.driver)
|
||||
actions.move_to_element(easy_apply_button).click().perform()
|
||||
self.gpt_answerer.set_job(job)
|
||||
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:
|
||||
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")]')
|
||||
)
|
||||
)
|
||||
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"]')
|
||||
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
|
||||
return description
|
||||
except NoSuchElementException:
|
||||
tb_str = traceback.format_exc()
|
||||
raise Exception("Job description 'See more' button not found: \nTraceback:\n{tb_str}")
|
||||
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)
|
||||
|
||||
def _fill_application_form(self, job):
|
||||
while True:
|
||||
self.fill_up(job)
|
||||
if self._next_or_submit():
|
||||
break
|
||||
|
||||
def _next_or_submit(self):
|
||||
next_button = self.driver.find_element(By.CLASS_NAME, "artdeco-button--primary")
|
||||
button_text = next_button.text.lower()
|
||||
if 'submit application' in button_text:
|
||||
self._unfollow_company()
|
||||
time.sleep(random.uniform(1.5, 2.5))
|
||||
next_button.click()
|
||||
time.sleep(random.uniform(1.5, 2.5))
|
||||
return True
|
||||
time.sleep(random.uniform(1.5, 2.5))
|
||||
next_button.click()
|
||||
time.sleep(random.uniform(3.0, 5.0))
|
||||
self._check_for_errors()
|
||||
|
||||
def _unfollow_company(self) -> None:
|
||||
try:
|
||||
follow_checkbox = self.driver.find_element(
|
||||
By.XPATH, "//label[contains(.,'to stay up to date with their page.')]")
|
||||
follow_checkbox.click()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _check_for_errors(self) -> None:
|
||||
error_elements = self.driver.find_elements(By.CLASS_NAME, 'artdeco-inline-feedback--error')
|
||||
if error_elements:
|
||||
raise Exception(f"Failed answering or file upload. {str([e.text for e in error_elements])}")
|
||||
|
||||
def _discard_application(self) -> None:
|
||||
try:
|
||||
self.driver.find_element(By.CLASS_NAME, 'artdeco-modal__dismiss').click()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
self.driver.find_elements(By.CLASS_NAME, 'artdeco-modal__confirm-dialog-btn')[0].click()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
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, 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:
|
||||
return bool(element.find_elements(By.XPATH, ".//input[@type='file']"))
|
||||
|
||||
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)
|
||||
output = self.gpt_answerer.resume_or_cover(parent.text.lower())
|
||||
if 'resume' in output:
|
||||
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)
|
||||
|
||||
def _create_and_upload_resume(self, element, job):
|
||||
folder_path = 'generated_cv'
|
||||
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)
|
||||
_, height = letter
|
||||
text_object = c.beginText(100, height - 100)
|
||||
text_object.setFont("Helvetica", 12)
|
||||
text_object.textLines(cover_letter)
|
||||
c.drawText(text_object)
|
||||
c.save()
|
||||
element.send_keys(letter_path)
|
||||
|
||||
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_form_section(section)
|
||||
|
||||
|
||||
def _process_form_section(self, section: WebElement) -> None:
|
||||
if self._handle_terms_of_service(section):
|
||||
return
|
||||
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:
|
||||
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 _find_and_handle_radio_question(self, section: WebElement) -> bool:
|
||||
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]
|
||||
|
||||
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._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:
|
||||
text_field = text_fields[0]
|
||||
question_text = section.find_element(By.TAG_NAME, 'label').text.lower()
|
||||
is_numeric = self._is_numeric_field(text_field)
|
||||
if is_numeric:
|
||||
question_type = 'numeric'
|
||||
answer = self.gpt_answerer.answer_question_numeric(question_text)
|
||||
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:
|
||||
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
|
||||
|
||||
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]
|
||||
question_text = section.text.lower()
|
||||
answer_date = self.gpt_answerer.answer_question_date()
|
||||
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
|
||||
|
||||
def _find_and_handle_dropdown_question(self, section: WebElement) -> bool:
|
||||
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]
|
||||
|
||||
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._save_questions_to_json({'type': 'dropdown', 'question': question_text, 'answer': answer})
|
||||
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()
|
||||
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()
|
||||
element.send_keys(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()
|
||||
|
||||
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:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, list):
|
||||
raise ValueError("JSON file format is incorrect. Expected a list of questions.")
|
||||
except json.JSONDecodeError:
|
||||
data = []
|
||||
except FileNotFoundError:
|
||||
data = []
|
||||
data.append(question_data)
|
||||
with open(output_file, 'w') as f:
|
||||
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()
|
||||
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
|
||||
219
src/linkedIn_job_manager.py
Normal file
219
src/linkedIn_job_manager.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import os
|
||||
import random
|
||||
import time
|
||||
import traceback
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
import src.utils as utils
|
||||
from job import Job
|
||||
from src.linkedIn_easy_applier import LinkedInEasyApplier
|
||||
import json
|
||||
|
||||
|
||||
class EnvironmentKeys:
|
||||
def __init__(self):
|
||||
self.skip_apply = self._read_env_key_bool("SKIP_APPLY")
|
||||
self.disable_description_filter = self._read_env_key_bool("DISABLE_DESCRIPTION_FILTER")
|
||||
|
||||
@staticmethod
|
||||
def _read_env_key(key: str) -> str:
|
||||
return os.getenv(key, "")
|
||||
|
||||
@staticmethod
|
||||
def _read_env_key_bool(key: str) -> bool:
|
||||
return os.getenv(key) == "True"
|
||||
|
||||
class LinkedInJobManager:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
self.set_old_answers = set()
|
||||
self.easy_applier_component = None
|
||||
|
||||
def set_parameters(self, parameters):
|
||||
self.company_blacklist = parameters.get('companyBlacklist', []) or []
|
||||
self.title_blacklist = parameters.get('titleBlacklist', []) or []
|
||||
self.positions = parameters.get('positions', [])
|
||||
self.locations = parameters.get('locations', [])
|
||||
self.base_search_url = self.get_base_search_url(parameters)
|
||||
self.seen_jobs = []
|
||||
resume_path = parameters.get('uploads', {}).get('resume', None)
|
||||
if resume_path is not None and Path(resume_path).exists():
|
||||
self.resume_path = Path(resume_path)
|
||||
else:
|
||||
self.resume_path = None
|
||||
self.output_file_directory = Path(parameters['outputFileDirectory'])
|
||||
self.env_config = EnvironmentKeys()
|
||||
#self.old_question()
|
||||
|
||||
def set_gpt_answerer(self, gpt_answerer):
|
||||
self.gpt_answerer = gpt_answerer
|
||||
|
||||
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):
|
||||
with open(file_path, 'r', newline='', encoding='utf-8', errors='ignore') as file:
|
||||
csv_reader = csv.reader(file, delimiter=',', quotechar='"')
|
||||
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"""
|
||||
|
||||
|
||||
def start_applying(self):
|
||||
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
|
||||
minimum_time = 60 * 15
|
||||
minimum_page_time = time.time() + minimum_time
|
||||
|
||||
for position, location in searches:
|
||||
location_url = "&location=" + location
|
||||
job_page_number = -1
|
||||
utils.printyellow(f"Starting the search for {position} in {location}.")
|
||||
|
||||
try:
|
||||
while True:
|
||||
page_sleep += 1
|
||||
job_page_number += 1
|
||||
utils.printyellow(f"Going to job page {job_page_number}")
|
||||
self.next_job_page(position, location_url, job_page_number)
|
||||
time.sleep(random.uniform(1.5, 3.5))
|
||||
utils.printyellow("Starting the application process for this page...")
|
||||
self.apply_jobs()
|
||||
utils.printyellow("Applying to jobs on this page has been completed!")
|
||||
|
||||
time_left = minimum_page_time - time.time()
|
||||
if time_left > 0:
|
||||
utils.printyellow(f"Sleeping for {time_left} seconds.")
|
||||
time.sleep(time_left)
|
||||
minimum_page_time = time.time() + minimum_time
|
||||
if page_sleep % 5 == 0:
|
||||
sleep_time = random.randint(5, 34)
|
||||
utils.printyellow(f"Sleeping for {sleep_time / 60} minutes.")
|
||||
time.sleep(sleep_time)
|
||||
page_sleep += 1
|
||||
except Exception:
|
||||
traceback.format_exc()
|
||||
pass
|
||||
time_left = minimum_page_time - time.time()
|
||||
if time_left > 0:
|
||||
utils.printyellow(f"Sleeping for {time_left} seconds.")
|
||||
time.sleep(time_left)
|
||||
minimum_page_time = time.time() + minimum_time
|
||||
if page_sleep % 5 == 0:
|
||||
sleep_time = random.randint(50, 90)
|
||||
utils.printyellow(f"Sleeping for {sleep_time / 60} minutes.")
|
||||
time.sleep(sleep_time)
|
||||
page_sleep += 1
|
||||
|
||||
def apply_jobs(self):
|
||||
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
|
||||
|
||||
def write_to_file(self, job, file_name):
|
||||
pdf_path = Path(job.pdf_path).resolve()
|
||||
pdf_path = pdf_path.as_uri()
|
||||
data = {
|
||||
"company": job.company,
|
||||
"job_title": job.title,
|
||||
"link": job.link,
|
||||
"job_location": job.location,
|
||||
"pdf_path": 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 = []
|
||||
if parameters['remote']:
|
||||
url_parts.append("f_CF=f_WRA")
|
||||
experience_levels = [str(i+1) for i, v in enumerate(parameters.get('experienceLevel', [])) if v]
|
||||
if experience_levels:
|
||||
url_parts.append(f"f_E={','.join(experience_levels)}")
|
||||
url_parts.append(f"distance={parameters['distance']}")
|
||||
job_types = [key[0].upper() for key, value in parameters.get('jobTypes', {}).items() if value]
|
||||
if job_types:
|
||||
url_parts.append(f"f_JT={','.join(job_types)}")
|
||||
date_mapping = {
|
||||
"all time": "",
|
||||
"month": "&f_TPR=r2592000",
|
||||
"week": "&f_TPR=r604800",
|
||||
"24 hours": "&f_TPR=r86400"
|
||||
}
|
||||
date_param = next((v for k, v in date_mapping.items() if parameters.get('date', {}).get(k)), "")
|
||||
url_parts.append("f_LF=f_AL") # Easy Apply
|
||||
base_url = "&".join(url_parts)
|
||||
return f"?{base_url}{date_param}"
|
||||
|
||||
def next_job_page(self, position, location, job_page):
|
||||
self.driver.get(f"https://www.linkedin.com/jobs/search/{self.base_search_url}&keywords={position}{location}&start={job_page * 25}")
|
||||
|
||||
def extract_job_information_from_tile(self, job_tile):
|
||||
job_title, company, job_location, apply_method, link = "", "", "", "", ""
|
||||
try:
|
||||
job_title = job_tile.find_element(By.CLASS_NAME, 'job-card-list__title').text
|
||||
link = job_tile.find_element(By.CLASS_NAME, 'job-card-list__title').get_attribute('href').split('?')[0]
|
||||
company = job_tile.find_element(By.CLASS_NAME, 'job-card-container__primary-description').text
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
job_location = job_tile.find_element(By.CLASS_NAME, 'job-card-container__metadata-item').text
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
apply_method = job_tile.find_element(By.CLASS_NAME, 'job-card-container__apply-method').text
|
||||
except:
|
||||
apply_method = "Applied"
|
||||
|
||||
return job_title, company, job_location, link, apply_method
|
||||
|
||||
def is_blacklisted(self, job_title, company, link):
|
||||
job_title_words = job_title.lower().split(' ')
|
||||
title_blacklisted = any(word in job_title_words for word in self.title_blacklist)
|
||||
company_blacklisted = company.strip().lower() in (word.strip().lower() for word in self.company_blacklist)
|
||||
link_seen = link in self.seen_jobs
|
||||
return title_blacklisted or company_blacklisted or link_seen
|
||||
434
src/strings.py
Normal file
434
src/strings.py
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
# Personal Information Template
|
||||
personal_information_template = """
|
||||
Answer the following question based on the provided personal information.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: John Doe, born on 01/01/1990, living in Milan, Italy.
|
||||
Question: What is your city?
|
||||
Milan
|
||||
|
||||
Personal Information: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# Personal Information Template
|
||||
personal_information_template = """
|
||||
Answer the following question based on the provided personal information.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: John Doe, born on 01/01/1990, living in Milan, Italy.
|
||||
Question: What is your city?
|
||||
Milan
|
||||
|
||||
Personal Information: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Self Identification Template
|
||||
self_identification_template = """
|
||||
Answer the following question based on the provided self-identification details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: Male, uses he/him pronouns, not a veteran, no disability.
|
||||
Question: What are your gender?
|
||||
Male
|
||||
|
||||
Self-Identification: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Legal Authorization Template
|
||||
legal_authorization_template = """
|
||||
Answer the following question based on the provided legal authorization details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: Authorized to work in the EU, no US visa required.
|
||||
Question: Are you legally allowed to work in the EU?
|
||||
Yes
|
||||
|
||||
Legal Authorization: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Work Preferences Template
|
||||
work_preferences_template = """
|
||||
Answer the following question based on the provided work preferences.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: Open to remote work, willing to relocate.
|
||||
Question: Are you open to remote work?
|
||||
Yes
|
||||
|
||||
Work Preferences: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Education Details Template
|
||||
education_details_template = """
|
||||
Answer the following question based on the provided education details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Bachelor's degree in Computer Science with experience in Python.
|
||||
Question: Do you have experience with Python?
|
||||
Yes, I have experience with Python.
|
||||
|
||||
Education Details: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Experience Details Template
|
||||
experience_details_template = """
|
||||
Answer the following question based on the provided experience details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: 3 years as a software developer with leadership experience.
|
||||
Question: Do you have leadership experience?
|
||||
Yes, I have 3 years of leadership experience.
|
||||
|
||||
Experience Details: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Projects Template
|
||||
projects_template = """
|
||||
Answer the following question based on the provided project details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Led the development of a mobile app, repository available.
|
||||
Question: Have you led any projects?
|
||||
Yes, led the development of a mobile app
|
||||
|
||||
Projects: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Availability Template
|
||||
availability_template = """
|
||||
Answer the following question based on the provided availability details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- Keep the answer under 140 characters.
|
||||
- Use periods only if the answer has multiple sentences.
|
||||
|
||||
## Example
|
||||
My resume: Available to start immediately.
|
||||
Question: When can you start?
|
||||
I can start immediately.
|
||||
|
||||
Availability: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Salary Expectations Template
|
||||
salary_expectations_template = """
|
||||
Answer the following question based on the provided salary expectations.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- Keep the answer under 140 characters.
|
||||
- Use periods only if the answer has multiple sentences.
|
||||
|
||||
## Example
|
||||
My resume: Looking for a salary in the range of 50k-60k USD.
|
||||
Question: What are your salary expectations?
|
||||
55000.
|
||||
|
||||
Salary Expectations: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Certifications Template
|
||||
certifications_template = """
|
||||
Answer the following question based on the provided certifications.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Certified in Project Management Professional (PMP).
|
||||
Question: Do you have PMP certification?
|
||||
Yes, I am PMP certified.
|
||||
|
||||
Certifications: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Languages Template
|
||||
languages_template = """
|
||||
Answer the following question based on the provided language skills.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Fluent in Italian and English.
|
||||
Question: What languages do you speak?
|
||||
Fluent in Italian and English.
|
||||
|
||||
Languages: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Interests Template
|
||||
interests_template = """
|
||||
Answer the following question based on the provided interests.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- Keep the answer under 140 characters.
|
||||
- Use periods only if the answer has multiple sentences.
|
||||
|
||||
## Example
|
||||
My resume: Interested in AI and data science.
|
||||
Question: What are your interests?
|
||||
AI and data science.
|
||||
|
||||
Interests: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
summarize_prompt_template = """
|
||||
As a seasoned HR expert, your task is to identify and outline the key skills and requirements necessary for the position of this job. Use the provided job description as input to extract all relevant information. This will involve conducting a thorough analysis of the job's responsibilities and the industry standards. You should consider both the technical and soft skills needed to excel in this role. Additionally, specify any educational qualifications, certifications, or experiences that are essential. Your analysis should also reflect on the evolving nature of this role, considering future trends and how they might affect the required competencies.
|
||||
|
||||
Rules:
|
||||
Remove boilerplate text
|
||||
Include only relevant information to match the job description against the resume
|
||||
|
||||
# Analysis Requirements
|
||||
Your analysis should include the following sections:
|
||||
Technical Skills: List all the specific technical skills required for the role based on the responsibilities described in the job description.
|
||||
Soft Skills: Identify the necessary soft skills, such as communication abilities, problem-solving, time management, etc.
|
||||
Educational Qualifications and Certifications: Specify the essential educational qualifications and certifications for the role.
|
||||
Professional Experience: Describe the relevant work experiences that are required or preferred.
|
||||
Role Evolution: Analyze how the role might evolve in the future, considering industry trends and how these might influence the required skills.
|
||||
|
||||
# Final Result:
|
||||
Your analysis should be structured in a clear and organized document with distinct sections for each of the points listed above. Each section should contain:
|
||||
This comprehensive overview will serve as a guideline for the recruitment process, ensuring the identification of the most qualified candidates.
|
||||
|
||||
# Job Description:
|
||||
```
|
||||
{text}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Job Description Summary"""
|
||||
|
||||
|
||||
coverletter_template = """
|
||||
Compose a brief and impactful cover letter based on the provided job description and resume. The letter should be no longer than three paragraphs and should be written in a professional, yet conversational tone. Avoid using any placeholders, and ensure that the letter flows naturally and is tailored to the job.
|
||||
|
||||
Analyze the job description to identify key qualifications and requirements. Introduce the candidate succinctly, aligning their career objectives with the role. Highlight relevant skills and experiences from the resume that directly match the job’s demands, using specific examples to illustrate these qualifications. Reference notable aspects of the company, such as its mission or values, that resonate with the candidate’s professional goals. Conclude with a strong statement of why the candidate is a good fit for the position, expressing a desire to discuss further.
|
||||
|
||||
Please write the cover letter in a way that directly addresses the job role and the company’s characteristics, ensuring it remains concise and engaging without unnecessary embellishments. The letter should be formatted into paragraphs and should not include a greeting or signature.
|
||||
|
||||
## Rules:
|
||||
- Provide only the text of the cover letter.
|
||||
- Do not include any introductions, explanations, or additional information.
|
||||
- The letter should be formatted into paragraph.
|
||||
|
||||
## Job Description:
|
||||
```
|
||||
{job_description}
|
||||
```
|
||||
## My resume:
|
||||
```
|
||||
{resume}
|
||||
```
|
||||
"""
|
||||
|
||||
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:
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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 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.
|
||||
|
||||
|
||||
## Rules
|
||||
- Answer the question directly (only number).
|
||||
|
||||
## Example 1
|
||||
```
|
||||
## Curriculum
|
||||
|
||||
I had a degree in computer science. I have worked 2 years with MQTT protocol.
|
||||
|
||||
## Question
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
## Rules
|
||||
- Never choose the default/placeholder option, examples are: 'Select an option', 'None', 'Choose from the options below', etc.
|
||||
- The answer must be one of the options.
|
||||
- The answer must exclusively contain one of the options.
|
||||
|
||||
## Example
|
||||
My resume: I'm a software engineer with 10 years of experience on swift, python, C, C++.
|
||||
Question: How many years of experience do you have on python?
|
||||
Options: [1-2, 3-5, 6-10, 10+]
|
||||
10+
|
||||
|
||||
-----
|
||||
|
||||
## My resume:
|
||||
```
|
||||
{resume}
|
||||
```
|
||||
|
||||
## Question:
|
||||
{question}
|
||||
|
||||
## Options:
|
||||
{options}
|
||||
|
||||
## """
|
||||
|
||||
|
||||
try_to_fix_template = """\
|
||||
The objective is to fix the text of a form input on a web page.
|
||||
|
||||
## Rules
|
||||
- Use the error to fix the original text.
|
||||
- The error "Please enter a valid answer" usually means the text is too large, shorten the reply to less than a tweet.
|
||||
- For errors like "Enter a whole number between 3 and 30", just need a number.
|
||||
|
||||
-----
|
||||
|
||||
## Form Question
|
||||
{question}
|
||||
|
||||
## Input
|
||||
{input}
|
||||
|
||||
## Error
|
||||
{error}
|
||||
|
||||
## Fixed Input
|
||||
"""
|
||||
|
||||
func_summarize_prompt_template = """
|
||||
Following are two texts, one with placeholders and one without, the second text uses information from the first text to fill the placeholders.
|
||||
|
||||
## Rules
|
||||
- A placeholder is a string like "[[placeholder]]". E.g. "[[company]]", "[[job_title]]", "[[years_of_experience]]"...
|
||||
- The task is to remove the placeholders from the text.
|
||||
- If there is no information to fill a placeholder, remove the placeholder, and adapt the text accordingly.
|
||||
- No placeholders should remain in the text.
|
||||
|
||||
## Example
|
||||
Text with placeholders: "I'm a software engineer engineer with 10 years of experience on [placeholder] and [placeholder]."
|
||||
Text without placeholders: "I'm a software engineer with 10 years of experience."
|
||||
|
||||
-----
|
||||
|
||||
## Text with placeholders:
|
||||
{text_with_placeholders}
|
||||
|
||||
## Text without placeholders:"""
|
||||
103
src/utils.py
Normal file
103
src/utils.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import os
|
||||
import random
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
|
||||
chromeProfilePath = os.path.join(os.getcwd(), "chrome_profile", "linkedin_profile")
|
||||
|
||||
def ensure_chrome_profile():
|
||||
profile_dir = os.path.dirname(chromeProfilePath)
|
||||
if not os.path.exists(profile_dir):
|
||||
os.makedirs(profile_dir)
|
||||
if not os.path.exists(chromeProfilePath):
|
||||
os.makedirs(chromeProfilePath)
|
||||
return chromeProfilePath
|
||||
|
||||
def is_scrollable(element):
|
||||
scroll_height = element.get_attribute("scrollHeight")
|
||||
client_height = element.get_attribute("clientHeight")
|
||||
return int(scroll_height) > int(client_height)
|
||||
|
||||
def scroll_slow(driver, scrollable_element, start=0, end=3600, step=100, reverse=False):
|
||||
if reverse:
|
||||
start, end = end, start
|
||||
step = -step
|
||||
if step == 0:
|
||||
raise ValueError("Step cannot be zero.")
|
||||
script_scroll_to = "arguments[0].scrollTop = arguments[1];"
|
||||
try:
|
||||
if scrollable_element.is_displayed():
|
||||
if not is_scrollable(scrollable_element):
|
||||
print("The element is not scrollable.")
|
||||
return
|
||||
if (step > 0 and start >= end) or (step < 0 and start <= end):
|
||||
print("No scrolling will occur due to incorrect start/end values.")
|
||||
return
|
||||
for position in range(start, end, step):
|
||||
try:
|
||||
driver.execute_script(script_scroll_to, scrollable_element, position)
|
||||
except Exception as e:
|
||||
print(f"Error during scrolling: {e}")
|
||||
time.sleep(random.uniform(1.0, 2.6))
|
||||
driver.execute_script(script_scroll_to, scrollable_element, end)
|
||||
time.sleep(1)
|
||||
else:
|
||||
print("The element is not visible.")
|
||||
except Exception as e:
|
||||
print(f"Exception occurred: {e}")
|
||||
|
||||
def chromeBrowserOptions():
|
||||
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("--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)
|
||||
profileDir = os.path.basename(chromeProfilePath)
|
||||
options.add_argument('--user-data-dir=' + initialPath)
|
||||
options.add_argument("--profile-directory=" + profileDir)
|
||||
else:
|
||||
options.add_argument("--incognito")
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def printred(text):
|
||||
# Codice colore ANSI per il rosso
|
||||
RED = "\033[91m"
|
||||
RESET = "\033[0m"
|
||||
# Stampa il testo in rosso
|
||||
print(f"{RED}{text}{RESET}")
|
||||
|
||||
def printyellow(text):
|
||||
# Codice colore ANSI per il giallo
|
||||
YELLOW = "\033[93m"
|
||||
RESET = "\033[0m"
|
||||
# Stampa il testo in giallo
|
||||
print(f"{YELLOW}{text}{RESET}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue