commit
3bc6a89364
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,9 +6,8 @@ test*
|
||||
openaiSelenium*
|
||||
open_ai_calls.json
|
||||
_*
|
||||
data_folder*
|
||||
.venv
|
||||
generated_cv*
|
||||
resume.html
|
||||
.vscode
|
||||
chrome_profile
|
||||
answers.json
|
||||
|
305
README.md
305
README.md
@ -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`
|
||||
|
||||
- `education_details:`
|
||||
- List your educational background
|
||||
- Include degree, university, GPA, graduation year, field of study, and skills acquired
|
||||
- Example:
|
||||
##### Example
|
||||
```yaml
|
||||
- degree: "Bachelor's"
|
||||
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
|
||||
|
||||
##### Description
|
||||
This section outlines your academic background, including degrees earned and relevant coursework.
|
||||
|
||||
- **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"
|
||||
graduationYear: "2022"
|
||||
fieldOfStudy: "Computer Science"
|
||||
skillsAcquired:
|
||||
problemSolving: "4"
|
||||
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:`
|
||||
- List your work experiences
|
||||
- Include position, company, employment period, location, industry, key responsibilities, and skills acquired
|
||||
- Example:
|
||||
#### 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 Corp"
|
||||
employmentPeriod: "Jan 2020 - Present"
|
||||
location: "San Francisco, USA"
|
||||
company: "Tech Innovations Inc."
|
||||
employment_period: "06/2021 - Present"
|
||||
location: "San Francisco, CA"
|
||||
industry: "Technology"
|
||||
keyResponsibilities:
|
||||
responsibility1: "Developed web applications using React"
|
||||
skillsAcquired:
|
||||
adaptability: "3"
|
||||
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"
|
||||
```
|
||||
|
||||
- Other sections like `projects`, `availability`, `salary_expectations`, `certifications`, `skills`, `languages`, and `interests` follow a similar format, with each item on a new line.
|
||||
#### 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.
|
||||
|
||||
|
@ -24,7 +24,8 @@ date:
|
||||
24 hours: [true/false]
|
||||
|
||||
positions:
|
||||
- Software developer
|
||||
- position1
|
||||
- position2
|
||||
|
||||
locations:
|
||||
- Country1
|
||||
|
@ -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]"
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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"
|
@ -1 +0,0 @@
|
||||
Hi, I'm working on this one!
|
@ -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()
|
@ -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
|
188
main.py
188
main.py
@ -1,112 +1,111 @@
|
||||
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}")
|
||||
|
||||
|
||||
# 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.")
|
||||
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 '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}.")
|
||||
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 '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}.")
|
||||
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 '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}.")
|
||||
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 '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}.")
|
||||
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 '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}.")
|
||||
@ -120,32 +119,22 @@ 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:
|
||||
@ -154,14 +143,14 @@ class FileManager:
|
||||
|
||||
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,31 +160,45 @@ 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
|
||||
|
||||
@ -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)}")
|
||||
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
127
resume.py
127
resume.py
@ -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)
|
||||
)
|
@ -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> => <TAG*gt;
|
||||
md.formatTag = function (html) { return html.replace(/</g,'<').replace(/\>/g,'>'); }
|
||||
|
||||
// frontmatter for simple YAML (support multi-level, but string value only)
|
||||
md.formatYAML = function (front, matter) {
|
||||
var level = {}, latest = md.yaml;
|
||||
matter.replace( /^\s*#(.*)$/gm, '' ).replace( /^( *)([^:^\n]+):(.*)$/gm, function(m, sp, key,val) {
|
||||
level[sp] = level[sp] || latest
|
||||
latest = level[sp][key.trim()] = val.trim() || {}
|
||||
for (e in level) if(e>sp) level[e]=null;
|
||||
} );
|
||||
return ''
|
||||
}
|
||||
|
||||
//===== format code-block, highlight remarks/keywords for code/sql
|
||||
md.formatCode = function (match, title, block) {
|
||||
// convert tag <> to < > tab to 3 space, support marker using ^^^
|
||||
block = block.replace(/</g,'<').replace(/\>/g,'>')
|
||||
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,'`') + '</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)}() );
|
@ -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);
|
@ -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 }
|
@ -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,9 +144,10 @@ 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(
|
||||
@ -161,35 +158,6 @@ class GPTAnswerer:
|
||||
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()
|
||||
@ -211,12 +179,94 @@ 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})
|
||||
@ -225,26 +275,19 @@ class GPTAnswerer:
|
||||
chain = chains.get(section_name)
|
||||
output = chain.invoke({"resume": self.resume, "job_description": self.job_description})
|
||||
return output
|
||||
resume_section = getattr(self.resume, section_name, None)
|
||||
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"
|
@ -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.'}
|
132
src/job_application_profile.py
Normal file
132
src/job_application_profile.py
Normal file
@ -0,0 +1,132 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List
|
||||
import yaml
|
||||
|
||||
@dataclass
|
||||
class SelfIdentification:
|
||||
gender: str
|
||||
pronouns: str
|
||||
veteran: str
|
||||
disability: str
|
||||
ethnicity: str
|
||||
|
||||
@dataclass
|
||||
class LegalAuthorization:
|
||||
eu_work_authorization: str
|
||||
us_work_authorization: str
|
||||
requires_us_visa: str
|
||||
legally_allowed_to_work_in_us: str
|
||||
requires_us_sponsorship: str
|
||||
requires_eu_visa: str
|
||||
legally_allowed_to_work_in_eu: str
|
||||
requires_eu_sponsorship: str
|
||||
|
||||
@dataclass
|
||||
class WorkPreferences:
|
||||
remote_work: str
|
||||
in_person_work: str
|
||||
open_to_relocation: str
|
||||
willing_to_complete_assessments: str
|
||||
willing_to_undergo_drug_tests: str
|
||||
willing_to_undergo_background_checks: str
|
||||
|
||||
@dataclass
|
||||
class Availability:
|
||||
notice_period: str
|
||||
|
||||
@dataclass
|
||||
class SalaryExpectations:
|
||||
salary_range_usd: str
|
||||
|
||||
@dataclass
|
||||
class JobApplicationProfile:
|
||||
self_identification: SelfIdentification
|
||||
legal_authorization: LegalAuthorization
|
||||
work_preferences: WorkPreferences
|
||||
availability: Availability
|
||||
salary_expectations: SalaryExpectations
|
||||
|
||||
def __init__(self, yaml_str: str):
|
||||
try:
|
||||
data = yaml.safe_load(yaml_str)
|
||||
except yaml.YAMLError as e:
|
||||
raise ValueError("Error parsing YAML file.") from e
|
||||
except Exception as e:
|
||||
raise RuntimeError("An unexpected error occurred while parsing the YAML file.") from e
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("YAML data must be a dictionary.")
|
||||
|
||||
# Process self_identification
|
||||
try:
|
||||
self.self_identification = SelfIdentification(**data['self_identification'])
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Required field {e} is missing in self_identification data.") from e
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Error in self_identification data: {e}") from e
|
||||
except AttributeError as e:
|
||||
raise AttributeError("Attribute error in self_identification processing.") from e
|
||||
except Exception as e:
|
||||
raise RuntimeError("An unexpected error occurred while processing self_identification.") from e
|
||||
|
||||
# Process legal_authorization
|
||||
try:
|
||||
self.legal_authorization = LegalAuthorization(**data['legal_authorization'])
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Required field {e} is missing in legal_authorization data.") from e
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Error in legal_authorization data: {e}") from e
|
||||
except AttributeError as e:
|
||||
raise AttributeError("Attribute error in legal_authorization processing.") from e
|
||||
except Exception as e:
|
||||
raise RuntimeError("An unexpected error occurred while processing legal_authorization.") from e
|
||||
|
||||
# Process work_preferences
|
||||
try:
|
||||
self.work_preferences = WorkPreferences(**data['work_preferences'])
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Required field {e} is missing in work_preferences data.") from e
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Error in work_preferences data: {e}") from e
|
||||
except AttributeError as e:
|
||||
raise AttributeError("Attribute error in work_preferences processing.") from e
|
||||
except Exception as e:
|
||||
raise RuntimeError("An unexpected error occurred while processing work_preferences.") from e
|
||||
|
||||
# Process availability
|
||||
try:
|
||||
self.availability = Availability(**data['availability'])
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Required field {e} is missing in availability data.") from e
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Error in availability data: {e}") from e
|
||||
except AttributeError as e:
|
||||
raise AttributeError("Attribute error in availability processing.") from e
|
||||
except Exception as e:
|
||||
raise RuntimeError("An unexpected error occurred while processing availability.") from e
|
||||
|
||||
# Process salary_expectations
|
||||
try:
|
||||
self.salary_expectations = SalaryExpectations(**data['salary_expectations'])
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Required field {e} is missing in salary_expectations data.") from e
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Error in salary_expectations data: {e}") from e
|
||||
except AttributeError as e:
|
||||
raise AttributeError("Attribute error in salary_expectations processing.") from e
|
||||
except Exception as e:
|
||||
raise RuntimeError("An unexpected error occurred while processing salary_expectations.") from e
|
||||
|
||||
# Process additional fields
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
def format_dataclass(obj):
|
||||
return "\n".join(f"{field.name}: {getattr(obj, field.name)}" for field in obj.__dataclass_fields__.values())
|
||||
|
||||
return (f"Self Identification:\n{format_dataclass(self.self_identification)}\n\n"
|
||||
f"Legal Authorization:\n{format_dataclass(self.legal_authorization)}\n\n"
|
||||
f"Work Preferences:\n{format_dataclass(self.work_preferences)}\n\n"
|
||||
f"Availability: {self.availability.notice_period}\n\n"
|
||||
f"Salary Expectations: {self.salary_expectations.salary_range_usd}\n\n")
|
@ -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'
|
73
src/linkedIn_bot_facade.py
Normal file
73
src/linkedIn_bot_facade.py
Normal file
@ -0,0 +1,73 @@
|
||||
class LinkedInBotState:
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.credentials_set = False
|
||||
self.api_key_set = False
|
||||
self.job_application_profile_set = False
|
||||
self.gpt_answerer_set = False
|
||||
self.parameters_set = False
|
||||
self.logged_in = False
|
||||
|
||||
def validate_state(self, required_keys):
|
||||
for key in required_keys:
|
||||
if not getattr(self, key):
|
||||
raise ValueError(f"{key.replace('_', ' ').capitalize()} must be set before proceeding.")
|
||||
|
||||
class LinkedInBotFacade:
|
||||
def __init__(self, login_component, apply_component):
|
||||
self.login_component = login_component
|
||||
self.apply_component = apply_component
|
||||
self.state = LinkedInBotState()
|
||||
self.job_application_profile = None
|
||||
self.resume = None
|
||||
self.email = None
|
||||
self.password = None
|
||||
self.parameters = None
|
||||
|
||||
def set_job_application_profile_and_resume(self, job_application_profile, resume):
|
||||
self._validate_non_empty(job_application_profile, "Job application profile")
|
||||
self._validate_non_empty(resume, "Resume")
|
||||
self.job_application_profile = job_application_profile
|
||||
self.resume = resume
|
||||
self.state.job_application_profile_set = True
|
||||
|
||||
def set_secrets(self, email, password):
|
||||
self._validate_non_empty(email, "Email")
|
||||
self._validate_non_empty(password, "Password")
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.state.credentials_set = True
|
||||
|
||||
def set_gpt_answerer_and_resume_generator(self, gpt_answerer_component, resume_generator_manager):
|
||||
self._ensure_job_profile_and_resume_set()
|
||||
gpt_answerer_component.set_job_application_profile(self.job_application_profile)
|
||||
gpt_answerer_component.set_resume(self.resume)
|
||||
self.apply_component.set_gpt_answerer(gpt_answerer_component)
|
||||
self.apply_component.set_resume_generator_manager(resume_generator_manager)
|
||||
self.state.gpt_answerer_set = True
|
||||
|
||||
def set_parameters(self, parameters):
|
||||
self._validate_non_empty(parameters, "Parameters")
|
||||
self.parameters = parameters
|
||||
self.apply_component.set_parameters(parameters)
|
||||
self.state.parameters_set = True
|
||||
|
||||
def start_login(self):
|
||||
self.state.validate_state(['credentials_set'])
|
||||
self.login_component.set_secrets(self.email, self.password)
|
||||
self.login_component.start()
|
||||
self.state.logged_in = True
|
||||
|
||||
def start_apply(self):
|
||||
self.state.validate_state(['logged_in', 'job_application_profile_set', 'gpt_answerer_set', 'parameters_set'])
|
||||
self.apply_component.start_applying()
|
||||
|
||||
def _validate_non_empty(self, value, name):
|
||||
if not value:
|
||||
raise ValueError(f"{name} cannot be empty.")
|
||||
|
||||
def _ensure_job_profile_and_resume_set(self):
|
||||
if not self.state.job_application_profile_set:
|
||||
raise ValueError("Job application profile and resume must be set before proceeding.")
|
398
src/linkedIn_easy_applier.py
Normal file
398
src/linkedIn_easy_applier.py
Normal 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
|
@ -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
|
||||
@ -116,7 +114,6 @@ class LinkedInJobManager:
|
||||
page_sleep += 1
|
||||
|
||||
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():
|
||||
@ -127,50 +124,49 @@ class LinkedInJobManager:
|
||||
job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list")
|
||||
utils.scroll_slow(self.driver, job_results)
|
||||
utils.scroll_slow(self.driver, job_results, step=300, reverse=True)
|
||||
|
||||
job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item')
|
||||
|
||||
if not job_list_elements:
|
||||
raise Exception("No job class elements found on page")
|
||||
|
||||
job_list = [Job(*self.extract_job_information_from_tile(job_element)) for job_element in job_list_elements]
|
||||
|
||||
for job in job_list:
|
||||
if self.is_blacklisted(job.title, job.company, job.link):
|
||||
utils.printyellow(f"Blacklisted {job.title} at {job.company}, skipping...")
|
||||
self.write_to_file(job.company, job.location, job.title, job.link, "skipped")
|
||||
self.write_to_file(job, "skipped")
|
||||
continue
|
||||
|
||||
try:
|
||||
if job.apply_method not in {"Continue", "Applied", "Apply"}:
|
||||
self.easy_applier_component.job_apply(job)
|
||||
self.write_to_file(job, "success")
|
||||
except Exception as e:
|
||||
utils.printred(traceback.format_exc())
|
||||
self.write_to_file(job.company, job.location, job.title, job.link, "failed")
|
||||
self.write_to_file(job, "failed")
|
||||
continue
|
||||
self.write_to_file(job.company, job.location, job.title, job.link, "success")
|
||||
|
||||
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"
|
||||
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:
|
||||
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}")
|
||||
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:
|
@ -1,144 +1,3 @@
|
||||
resume_markdown_template = """
|
||||
Act as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is twofold:
|
||||
|
||||
1. **Review and Extract Information**: Carefully examine the candidate's current resume to extract the following critical details:
|
||||
- Work experience
|
||||
- Educational background
|
||||
- Relevant skills
|
||||
- Achievements
|
||||
- Certifications
|
||||
|
||||
2. **Optimize the Resume**: Using the provided template, create a highly optimized resume for the relevant industry. The resume should:
|
||||
- Include commonly required skills and keywords for the industry
|
||||
- Utilize ATS-friendly phrases and terminology to ensure compatibility with automated systems
|
||||
- Highlight strengths and achievements relevant to the industry
|
||||
- Present experience, skills, and accomplishments in a compelling and professional manner
|
||||
- Maintain a clear, that is easily readable by both ATS and human reviewers
|
||||
|
||||
Provide guidance on how to enhance the presentation of the information to maximize impact and readability. Offer advice on tailoring the content to general industry standards, ensuring the resume passes ATS filters and captures the attention of recruiters, thereby increasing the candidate’s chances of securing an interview.
|
||||
|
||||
## Information to Collect and Analyze
|
||||
- **My information eesume:**
|
||||
{resume}
|
||||
|
||||
## Template to Use
|
||||
```
|
||||
# [Full Name]
|
||||
|
||||
[Your City, Your Country](Maps link)
|
||||
[Your Prefix Phone number](tel: Your Prefix Phone number)
|
||||
[Your Email](mailto:Your Email)
|
||||
[LinkedIn](Link LinkedIn account)
|
||||
[GitHub](Link GitHub account)
|
||||
|
||||
## Summary
|
||||
|
||||
[Brief professional summary highlighting your experience, key skills, and career objectives. 2-3 sentences.]
|
||||
|
||||
## Skills
|
||||
|
||||
- **Skill1:** [details (max 15 word)]
|
||||
- **Skill2:** [details (max 15 word)]
|
||||
- **Skill3:** [details (max 15 word)]
|
||||
- **Skill4:** [details (max 15 word)]
|
||||
- **Skill4:** [details (max 15 word)]
|
||||
- **Skill5:** [details (max 15 word)]
|
||||
|
||||
## Working Experience
|
||||
|
||||
### [Job Title]
|
||||
**[Company Name]** – [City, State]
|
||||
*[Start Date – End Date]*
|
||||
|
||||
1. [Achievement or responsibility]
|
||||
2. [Achievement or responsibility]
|
||||
3. [Achievement or responsibility]
|
||||
4. [Achievement or responsibility]
|
||||
5. [Achievement or responsibility]
|
||||
|
||||
### [Job Title]
|
||||
**[Company Name]** – [City, State]
|
||||
*[Start Date – End Date]*
|
||||
|
||||
1. [Achievement or responsibility]
|
||||
2. [Achievement or responsibility]
|
||||
3. [Achievement or responsibility]
|
||||
4. [Achievement or responsibility]
|
||||
5. [Achievement or responsibility]
|
||||
|
||||
### [Job Title]
|
||||
**[Company Name]** – [City, State]
|
||||
*[Start Date – End Date]*
|
||||
|
||||
1. [Achievement or responsibility]
|
||||
2. [Achievement or responsibility]
|
||||
3. [Achievement or responsibility]
|
||||
4. [Achievement or responsibility]
|
||||
5. [Achievement or responsibility]
|
||||
|
||||
## Education
|
||||
|
||||
**[Degree] in [Field of Study]**
|
||||
[University Name] – [City, State]
|
||||
*Graduated: [Month Year]*
|
||||
|
||||
## Certifications
|
||||
|
||||
1. [Certification Name]
|
||||
2. [Certification Name]
|
||||
3. [Certification Name]
|
||||
|
||||
## Projects
|
||||
|
||||
### [Project Name]
|
||||
1. [Brief description of the project and your role]
|
||||
|
||||
### [Project Name]
|
||||
1. [Brief description of the project and your role]
|
||||
|
||||
### [Project Name]
|
||||
1. [Brief description of the project and your role]
|
||||
|
||||
## Languages
|
||||
|
||||
1. **[Language]:** [Proficiency Level]
|
||||
2. **[Language]:** [Proficiency Level]
|
||||
```
|
||||
The results should be provided in **markdown** format, Provide only the markdown code for the resume, without any explanations or additional text and also without ```markdown ```
|
||||
"""
|
||||
|
||||
fusion_job_description_resume_template = """
|
||||
|
||||
Act as an HR expert and resume writer with a strategic approach. Customize the resume to highlight the candidate’s
|
||||
strengths, skills, and achievements that are most relevant to the provided job description.
|
||||
Use a smart and targeted approach, incorporating key skills and abilities as well as important aspects of the job
|
||||
description into the resume.
|
||||
Ensure that the resume grabs the attention of hiring managers within the first few seconds and uses specific keywords and phrases from the job description to pass through Applicant Tracking Systems (ATS).
|
||||
|
||||
Important Note: While making the necessary adjustments to align the resume with the job description, ensure that the overall structure of the resume remains intact. Do not drastically alter the organization of the document, but optimize it to highlight the most relevant points for the desired position.
|
||||
|
||||
- **Most important infomation on job descrption:**
|
||||
{job_description}
|
||||
|
||||
- **My information resume:**
|
||||
{formatted_resume}
|
||||
|
||||
The results should be provided in **markdown** format, Provide only the markdown code for the resume, without any explanations or additional text and also without ```markdown ```
|
||||
"""
|
||||
|
||||
|
||||
|
||||
html_template = """
|
||||
<!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 candidate’s projects and studies to infer skills not explicitly mentioned. Complex and advanced projects often indicate deeper expertise. For instance, if a project involves MQTT, you might infer IoT experience even if it's not explicitly mentioned.
|
||||
|
||||
2. **Indirect Experience and Academic Background:**
|
||||
- **Relevant Projects:** Consider the types of projects the candidate has worked on and the time spent on each project. Advanced projects suggest deeper skills. For example, a project involving MQTT packet parsing suggests MQTT and possibly IoT skills.
|
||||
- **Roles and Responsibilities:** Evaluate the roles and responsibilities held. If a role suggests knowledge of specific technologies or skills, provide a number based on that experience.
|
||||
- **Type of University and Studies:** Also consider the type of university and the duration of studies.
|
||||
|
||||
3. **Inference Over Default Response:** Always strive to infer experience based on the available information. If direct experience cannot be confirmed, use related skills, projects, and academic background to estimate a plausible number of years. Avoid defaulting to 0 if you can infer any relevant experience.
|
||||
|
||||
4. **Handling Experience Estimates:**
|
||||
- **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Aim to keep the values as high as possible avoid using "0" as a response unless absolutely necessary.
|
||||
- **For High Experience:** For high levels of experience, ensure the number provided is as certain as possible and based on clear evidence from the resume. Avoid making inferences for high experience levels unless the evidence is strong.
|
||||
|
||||
|
||||
## Rules
|
||||
- Answer the question directly (only number).
|
||||
- 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.
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user