Merge pull request #60 from feder-cr/v2

V2
This commit is contained in:
Federico 2024-08-23 22:35:08 +01:00 committed by GitHub
commit 3bc6a89364
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1490 additions and 1706 deletions

5
.gitignore vendored
View File

@ -6,9 +6,8 @@ test*
openaiSelenium*
open_ai_calls.json
_*
data_folder*
.venv
generated_cv*
resume.html
.vscode
chrome_profile
chrome_profile
answers.json

317
README.md
View File

@ -174,55 +174,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
@ -246,7 +472,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**
@ -275,7 +500,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
@ -291,7 +516,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.

View File

@ -24,7 +24,8 @@ date:
24 hours: [true/false]
positions:
- Software developer
- position1
- position2
locations:
- Country1
@ -38,4 +39,4 @@ companyBlacklist:
titleBlacklist:
- word1
- word2
- word2

View File

@ -1,103 +1,101 @@
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: "[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/Master's/Ph.D.]"
university: "[Name of University]"
- degree: "[Your Degree]"
university: "[Your 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]"
graduation_year: "[Year of Graduation]"
field_of_study: "[Your Field of Study]"
exam:
[Course Name 1]: "[Grade]"
[Course Name 2]: "[Grade]"
[Course Name 3]: "[Grade]"
[Course Name 4]: "[Grade]"
[Course Name 5]: "[Grade]"
experience_details:
- position: "[Job Title]"
- position: "[Your 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]"
employment_period: "[Start Date] - [End Date]"
location: "[Location]"
industry: "[Industry]"
key_responsibilities:
- responsibility_1: "[Key Responsibility 1]"
- responsibility_2: "[Key Responsibility 2]"
- responsibility_3: "[Key Responsibility 3]"
skills_acquired:
- "[Skill 1]"
- "[Skill 2]"
- "[Skill 3]"
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: "[Project Name]"
description: "[Brief Description of the Project]"
link: "[Project URL]"
- name: "[Project Name]"
description: "[Brief Description of the Project]"
link: "[Project URL]"
availability:
noticePeriod: "[Specify notice period]"
salary_expectations:
salaryRangeUSD: "[Specify your salary expectations in USD]"
achievements:
- name: "[Achievement Title]"
description: "[Brief Description of the Achievement]"
- name: "[Achievement Title]"
description: "[Brief Description of the Achievement]"
certifications:
- "[Certification 1]"
- "[Certification 2]"
- "[Certification 3]"
skills:
problemSolving: "[Years]"
criticalThinking: "[Years]"
adaptability: "[Years]"
perfectionism: "[Years]"
yourSkill: "[Years]"
yourSkill: "[Years]"
- "[Certification Name]"
languages:
- language: "Italian"
proficiency: "Native"
- language: "English"
proficiency: "Professional"
- language: "[Language Name]"
proficiency: "[Proficiency Level]"
- language: "[Language Name]"
proficiency: "[Proficiency Level]"
interests:
- "[List any hobbies or interests relevant to your professional profile]"
- "[Interest 1]"
- "[Interest 2]"
- "[Interest 3]"
- "[Interest 4]"
- "[Interest 5]"
availability:
notice_period: "[Notice Period]"
salary_expectations:
salary_range_usd: "[Expected Salary Range in USD]"
self_identification:
gender: "[Gender]"
pronouns: "[Pronouns]"
veteran: "[Veteran Status]"
disability: "[Disability Status]"
ethnicity: "[Ethnicity]"
legal_authorization:
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/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]"

View File

@ -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]
email: myemaillinkedin@gmail.com
password: ImpossiblePassowrd10
openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR

View File

@ -2,10 +2,10 @@ remote: true
experienceLevel:
internship: true
entry: false
entry: true
associate: true
mid-senior level: false
director: true
mid-senior level: true
director: false
executive: false
jobTypes:
@ -24,10 +24,11 @@ date:
24 hours: true
positions:
- Software developer
- Software Tester
locations:
- Germany
- USA
distance: 100

View File

@ -1,128 +1,133 @@
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/"
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/Him"
veteran: false
disability: false
ethnicity: "Mediterranean"
pronouns: "He"
veteran: "No"
disability: "No"
ethnicity: "white"
legal_authorization:
euWorkAuthorization: true
usWorkAuthorization: false
requiresUsVisa: true
legallyAllowedToWorkInUs: false
requiresUsSponsorship: true
requiresEuVisa: false
legallyAllowedToWorkInEu: true
requiresEuSponsorship: false
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:
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"
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"

View File

@ -1 +0,0 @@
Hi, I'm working on this one!

View File

@ -1,57 +0,0 @@
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
}
def set_resume(self, resume):
if not resume:
raise ValueError("Plain text resume cannot be empty.")
self.resume = resume
self.state["resume_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.")
self.email = email
self.password = password
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_parameters(self, parameters):
if not parameters:
raise ValueError("Parameters cannot be None or empty.")
self.parameters = parameters
self.apply_component.set_parameters(parameters)
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.login_component.set_secrets(self.email, self.password)
self.login_component.start()
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()

View File

@ -1,362 +0,0 @@
import base64
import os
import random
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
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
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
class LinkedInEasyApplier:
def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any):
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
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)
easy_apply_button.click()
self.gpt_answerer.set_job(job)
self._fill_application_form()
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}]')
)
)
except Exception as e:
pass
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()
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 :
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):
while True:
self.fill_up()
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) -> 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 _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 _is_upload_field(self, element: WebElement) -> bool:
try:
element.find_element(By.XPATH, ".//input[@type='file']")
return True
except NoSuchElementException:
return False
def _handle_upload_fields(self, element: WebElement) -> 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:
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():
self._create_and_upload_cover_letter(element)
else:
if self.resume_dir != None:
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)
def _create_and_upload_resume(self, element):
max_retries = 3
retry_delay = 1
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))
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
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_question(section)
def _process_question(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)
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
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()
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)
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
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()
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

204
main.py
View File

@ -1,113 +1,112 @@
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
from lib_resume_builder_AIHawk import Resume,StyleManager,FacadeManager,ResumeGenerator
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
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
# 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_config(config_yaml_path: Path) -> dict:
def validate_yaml_file(yaml_path: Path) -> dict:
try:
with open(config_yaml_path, 'r') as stream:
parameters = yaml.safe_load(stream)
with open(yaml_path, 'r') as stream:
return yaml.safe_load(stream)
except yaml.YAMLError as exc:
raise ConfigError(f"Error reading config file {config_yaml_path}: {exc}")
raise ConfigError(f"Error reading file {yaml_path}: {exc}")
except FileNotFoundError:
raise ConfigError(f"Config file not found: {config_yaml_path}")
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 = {
'remote': bool,
'experienceLevel': dict,
'jobTypes': dict,
'date': dict,
'positions': list,
'locations': list,
'distance': int,
'companyBlacklist': list,
'titleBlacklist': list
}
# 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.")
for key, expected_type in required_keys.items():
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}.")
# 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}.")
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 '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}.")
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 '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}.")
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 '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}.")
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 '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 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] = []
# 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 +119,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())
@ -171,34 +160,48 @@ def init_browser():
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()
os.system('cls' if os.name == 'nt' else 'clear')
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 = file.read()
resume_object = Resume(plain_text_resume_file)
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 +211,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)}")

Binary file not shown.

127
resume.py
View File

@ -1,127 +0,0 @@
from dataclasses import dataclass
from typing import Dict
import yaml
@dataclass
class PersonalInformation:
name: str
surname: str
dateOfBirth: str
country: str
city: str
address: str
phone: str
phonePrefix: str
email: str
github: str
linkedin: str
@dataclass
class SelfIdentification:
gender: str
pronouns: str
veteran: str
disability: str
ethnicity: str
@dataclass
class LegalAuthorization:
euWorkAuthorization: str
usWorkAuthorization: str
requiresUsVisa: str
legallyAllowedToWorkInUs: str
requiresUsSponsorship: str
requiresEuVisa: str
legallyAllowedToWorkInEu: str
requiresEuSponsorship: str
@dataclass
class WorkPreferences:
remoteWork: str
inPersonWork: str
openToRelocation: str
willingToCompleteAssessments: str
willingToUndergoDrugTests: str
willingToUndergoBackgroundChecks: str
@dataclass
class Education:
degree: str
university: str
gpa: str
graduationYear: str
fieldOfStudy: str
skillsAcquired: Dict[str, str]
@dataclass
class Experience:
position: str
company: str
employmentPeriod: str
location: str
industry: str
keyResponsibilities: Dict[str, str]
skillsAcquired: Dict[str, str]
@dataclass
class Availability:
noticePeriod: str
@dataclass
class SalaryExpectations:
salaryRangeUSD: str
@dataclass
class Language:
language: str
proficiency: str
class Resume:
def __init__(self, yaml_str: str):
data = yaml.safe_load(yaml_str)
self.personal_information = PersonalInformation(**data['personal_information'])
self.self_identification = SelfIdentification(**data['self_identification'])
self.legal_authorization = LegalAuthorization(**data['legal_authorization'])
self.work_preferences = WorkPreferences(**data['work_preferences'])
self.education_details = [Education(**edu) for edu in data['education_details']]
self.experience_details = [Experience(**exp) for exp in data['experience_details']]
self.projects = data['projects']
self.availability = Availability(**data['availability'])
self.salary_expectations = SalaryExpectations(**data['salary_expectations'])
self.certifications = data['certifications']
self.languages = [Language(**lang) for lang in data['languages']]
self.interests = data['interests']
def __str__(self):
def format_dict(dict_obj):
return "\n".join(f"{key}: {value}" for key, value in dict_obj.items())
def format_dataclass(obj):
return "\n".join(f"{field.name}: {getattr(obj, field.name)}" for field in obj.__dataclass_fields__.values())
return ("Personal Information:\n" + format_dataclass(self.personal_information) + "\n\n"
"Self Identification:\n" + format_dataclass(self.self_identification) + "\n\n"
"Legal Authorization:\n" + format_dataclass(self.legal_authorization) + "\n\n"
"Work Preferences:\n" + format_dataclass(self.work_preferences) + "\n\n"
"Education Details:\n" + "\n".join(
f" - {edu.degree} in {edu.fieldOfStudy} from {edu.university}, "
f"GPA: {edu.gpa}, Graduation Year: {edu.graduationYear}\n"
f" Skills Acquired:\n{format_dict(edu.skillsAcquired)}"
for edu in self.education_details
) + "\n\n"
"Experience Details:\n" + "\n".join(
f" - {exp.position} at {exp.company} ({exp.employmentPeriod}), {exp.location}, {exp.industry}\n"
f" Key Responsibilities:\n{format_dict(exp.keyResponsibilities)}\n"
f" Skills Acquired:\n{format_dict(exp.skillsAcquired)}"
for exp in self.experience_details
) + "\n\n"
"Projects:\n" + "\n".join(f" - {proj}" for proj in self.projects.values()) + "\n\n"
f"Availability: {self.availability.noticePeriod}\n\n"
f"Salary Expectations: {self.salary_expectations.salaryRangeUSD}\n\n"
"Certifications: " + ", ".join(self.certifications) + "\n\n"
"Languages:\n" + "\n".join(
f" - {lang.language} ({lang.proficiency})"
for lang in self.languages
) + "\n\n"
"Interests:\n" + ", ".join(self.interests)
)

View File

@ -1,202 +0,0 @@
/*****************************************************************************
* casual-markdown - a lightweight regexp-base markdown parser with TOC support
* 2022/07/31, v0.90, refine frontmatter (simple yaml)
* 2023/04/12, v0.92, addCopyButton for code-block
*
* Copyright (c) 2022-2023, Casualwriter (MIT Licensed)
* https://github.com/casualwriter/casual-markdown
*****************************************************************************/
;(function(){
// define md object, and extent function (which is a dummy function)
var md = { yaml:{}, before: function (str) {return str}, after: function (str) {return str} }
// function for REGEXP to convert html tag. ie. <TAG> => &lt;TAG*gt;
md.formatTag = function (html) { return html.replace(/</g,'&lt;').replace(/\>/g,'&gt;'); }
// 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 &lt; &gt; tab to 3 space, support marker using ^^^
block = block.replace(/</g,'&lt;').replace(/\>/g,'&gt;')
block = block.replace(/\t/g,' ').replace(/\^\^\^(.+?)\^\^\^/g, '<mark>$1</mark>')
// highlight comment and keyword based on title := none | sql | code
if (title.toLowerCase(title) == 'sql') {
block = block.replace(/^\-\-(.*)/gm,'<rem>--$1</rem>').replace(/\s\-\-(.*)/gm,' <rem>--$1</rem>')
block = block.replace(/(\s?)(function|procedure|return|if|then|else|end|loop|while|or|and|case|when)(\s)/gim,'$1<b>$2</b>$3')
block = block.replace(/(\s?)(select|update|delete|insert|create|from|where|group by|having|set)(\s)/gim,'$1<b>$2</b>$3')
} else if ((title||'none')!=='none') {
block = block.replace(/^\/\/(.*)/gm,'<rem>//$1</rem>').replace(/\s\/\/(.*)/gm,' <rem>//$1</rem>')
block = block.replace(/(\s?)(function|procedure|return|exit|if|then|else|end|loop|while|or|and|case|when)(\s)/gim,'$1<b>$2</b>$3')
block = block.replace(/(\s?)(var|let|const|=>|for|next|do|while|loop|continue|break|switch|try|catch|finally)(\s)/gim,'$1<b>$2</b>$3')
}
return '<pre title="' + title + '"><button onclick="md.clipboard(this)">copy</button><code>' + block + '</code></pre>'
}
// 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,'<th>')
var tbody = p2.replace(/^\|(.+)/gm,'$1').replace(/(.+)\|$/gm,'$1')
tbody = tbody.replace(/(.+)/gm,'<tr><td>$1</td></tr>').replace(/\|/g,'<td>')
return '\n<table>\n<thead>\n<th>' + thead + '\n</thead>\n<tbody>' + tbody + '\n</tbody></table>\n\n'
} )
// horizontal rule => <hr>
mdstr = mdstr.replace(/^-{3,}|^\_{3,}|^\*{3,}$/gm, '<hr>').replace(/\n\n<hr\>/g, '\n<br><hr>')
// header => <h1>..<h5>
mdstr = mdstr.replace(/^##### (.*?)\s*#*$/gm, '<h5>$1</h5>')
.replace(/^#### (.*?)\s*#*$/gm, '<h4>$1</h4>')
.replace(/^### (.*?)\s*#*$/gm, '<h3>$1</h3>')
.replace(/^## (.*?)\s*#*$/gm, '<h2>$1</h2>')
.replace(/^# (.*?)\s*#*$/gm, '<h1>$1</h1>')
.replace(/^<h(\d)\>(.*?)\s*{(.*)}\s*<\/h\d\>$/gm, '<h$1 id="$3">$2</h$1>')
// inline code-block: `code-block` => <code>code-block</code>
mdstr = mdstr.replace(/``(.*?)``/gm, function(m,p){ return '<code>' + md.formatTag(p).replace(/`/g,'&#96;') + '</code>'} )
mdstr = mdstr.replace(/`(.*?)`/gm, '<code>$1</code>' )
// blockquote, max 2 levels => <blockquote>{text}</blockquote>
mdstr = mdstr.replace(/^\>\> (.*$)/gm, '<blockquote><blockquote>$1</blockquote></blockquote>')
mdstr = mdstr.replace(/^\> (.*$)/gm, '<blockquote>$1</blockquote>')
mdstr = mdstr.replace(/<\/blockquote\>\n<blockquote\>/g, '\n<br>' )
mdstr = mdstr.replace(/<\/blockquote\>\n<br\><blockquote\>/g, '\n<br>' )
// image syntax: ![title](url) => <img alt="title" src="url" />
mdstr = mdstr.replace(/!\[(.*?)\]\((.*?) "(.*?)"\)/gm, '<img alt="$1" src="$2" $3 />')
mdstr = mdstr.replace(/!\[(.*?)\]\((.*?)\)/gm, '<img alt="$1" src="$2" width="90%" />')
// links syntax: [title "title"](url) => <a href="url" title="title">text</a>
mdstr = mdstr.replace(/\[(.*?)\]\((.*?) "new"\)/gm, '<a href="$2" target=_new>$1</a>')
mdstr = mdstr.replace(/\[(.*?)\]\((.*?) "(.*?)"\)/gm, '<a href="$2" title="$3">$1</a>')
mdstr = mdstr.replace(/([<\s])(https?\:\/\/.*?)([\s\>])/gm, '$1<a href="$2">$2</a>$3')
mdstr = mdstr.replace(/\[(.*?)\]\(\)/gm, '<a href="$1">$1</a>')
mdstr = mdstr.replace(/\[(.*?)\]\((.*?)\)/gm, '<a href="$2">$1</a>')
// unordered/ordered list, max 2 levels => <ul><li>..</li></ul>, <ol><li>..</li></ol>
mdstr = mdstr.replace(/^[\*+-][ .](.*)/gm, '<ul><li>$1</li></ul>' )
mdstr = mdstr.replace(/^\d\d?[ .](.*)/gm, '<ol><li>$1</li></ol>' )
mdstr = mdstr.replace(/^\s{2,6}[\*+-][ .](.*)/gm, '<ul><ul><li>$1</li></ul></ul>' )
mdstr = mdstr.replace(/^\s{2,6}\d[ .](.*)/gm, '<ul><ol><li>$1</li></ol></ul>' )
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, '<b><em>$1</em></b>')
mdstr = mdstr.replace(/\*\*(\w.*?[^\\])\*\*/gm, '<b>$1</b>')
mdstr = mdstr.replace(/\*(\w.*?[^\\])\*/gm, '<em>$1</em>')
mdstr = mdstr.replace(/___(\w.*?[^\\])___/gm, '<b><em>$1</em></b>')
mdstr = mdstr.replace(/__(\w.*?[^\\])__/gm, '<u>$1</u>')
// mdstr = mdstr.replace(/_(\w.*?[^\\])_/gm, '<u>$1</u>') // NOT support!!
mdstr = mdstr.replace(/\^\^\^(.+?)\^\^\^/gm, '<mark>$1</mark>')
mdstr = mdstr.replace(/\^\^(\w.*?)\^\^/gm, '<ins>$1</ins>')
mdstr = mdstr.replace(/~~(\w.*?)~~/gm, '<del>$1</del>')
// line break and paragraph => <br/> <p>
mdstr = mdstr.replace(/ \n/g, '\n<br/>').replace(/\n\s*\n/g, '\n<p>\n')
// indent as code-block
mdstr = mdstr.replace(/^ {4,10}(.*)/gm, function(m,p) { return '<pre><code>' + md.formatTag(p) + '</code></pre>'} )
mdstr = mdstr.replace(/^\t(.*)/gm, function(m,p) { return '<pre><code>' + md.formatTag(p) + '</code></pre>'} )
mdstr = mdstr.replace(/<\/code\><\/pre\>\n<pre\><code\>/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 "<code>", skip for code-block and process normal text
var pos1=0, pos2=0, mdHTML = ''
while ( (pos1 = mdText.indexOf('<code>')) >= 0 ) {
pos2 = mdText.indexOf('</code>', 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 '<div class="markdown">' + mdHTML + md.after( md.parser( md.before(mdText) ) ) + '</div>'
}
//===== 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 = '<div class="toc"><ul>' + (tocTitle=='none'? '' : '<h3>' + tocTitle + '</h3>');
// loop for each element,add <li> element with class in TAG name.
for (var i=0; i<toc.length; i++ ) {
if (toc[i].id.substr(0,6)=='no-toc') continue;
if (!toc[i].id) toc[i].id = "toc-item-" + i;
html += '<li class="' + toc[i].nodeName + '" title="#' + toc[i].id + '" onclick="location=this.title">'
html += toc[i].textContent + '</a></li>';
}
document.getElementById(tocDiv).innerHTML = html + "</ul>";
//===== 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; i<list.length; i++) {
var div = document.getElementById( list[i].title.substr(1) )
var pos = (div? div.offsetTop - divScroll.scrollTop + 10: 0 )
if ( pos>0 && pos<divHeight ) {
list[i].className = list[i].className.replace('active','') + ' active' // classList.add( 'active' );
} else {
list[i].className = list[i].className.replace('active','') // classList.remove( 'active' );
}
}
}
}
//===== end of scrollspy
}
if (typeof exports==='object') {
module.exports=md;
} else if (typeof define==='function') {
define(function(){return md;});
} else {
this.md=md;
}
}).call( function(){ return this||(typeof window!=='undefined'?window:global)}() );

View File

@ -1,43 +0,0 @@
// reorganizeHeader.js
function reorganizeHeader() {
const h1 = document.querySelector('h1');
if (!h1) return;
let currentNode = h1;
const headerInfoElements = [];
const contactInfoElements = [];
let firstAnchorFound = false;
while (currentNode && currentNode.tagName !== 'H2') {
if (currentNode.tagName === 'A') {
if (!firstAnchorFound) {
headerInfoElements.push(currentNode);
firstAnchorFound = true;
} else {
contactInfoElements.push(currentNode);
}
} else {
headerInfoElements.push(currentNode);
}
currentNode = currentNode.nextElementSibling;
}
const newHeader = document.createElement('header');
const headerInfoDiv = document.createElement('div');
headerInfoDiv.className = 'header-info';
headerInfoElements.forEach(el => {
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);

View File

@ -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 }

View File

@ -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
@ -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()
@ -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,115 @@ 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 +308,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"

View File

@ -9,6 +9,8 @@ class Job:
apply_method: str
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
@ -16,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.
@ -26,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.'}

View 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")

View File

@ -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'

View 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.")

View File

@ -0,0 +1,398 @@
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.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)
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 _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)
#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

View File

@ -1,4 +1,3 @@
import csv
import os
import random
import time
@ -7,9 +6,10 @@ 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
class EnvironmentKeys:
@ -40,20 +40,20 @@ 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()
#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_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
@ -117,60 +115,58 @@ 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.company, job.location, job.title, job.link, "skipped")
continue
try:
if job.apply_method not in {"Continue", "Applied", "Apply"}:
self.easy_applier_component.job_apply(job)
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")
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
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}")
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_recruiter": job.recruiter_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 = []
@ -205,12 +201,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:

View File

@ -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 candidates 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 candidates
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 = """
<!DOCTYPE html>
<title>Resume</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="{casual_markdown}"></script>
<script src="{reorganize_header}"></script>
<link rel="stylesheet" href="{resume_css}">
<body onload="document.body.innerHTML=md.html(document.body.innerHTML); document.body.style.display='block';">
"""
# Personal Information Template
personal_information_template = """
Answer the following question based on the provided personal information.
@ -420,61 +279,88 @@ 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 candidates 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).
- 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 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.

View File

@ -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,35 @@ 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("--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)
@ -123,7 +84,7 @@ def chromeBrowserOptions():
options.add_argument("--profile-directory=" + profileDir)
else:
options.add_argument("--incognito")
return options