diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..3404517 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'Stale issue message' + stale-pr-message: 'Stale pull request message' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/README.md b/README.md index e5e0206..310aa7b 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,10 @@ locations: - # Location you want to search for - # A second location you want to search in -cover_letter_loctn: -- # '/home/PATH_TO_FILE' +uploads: + Resume: # PATH TO Resume + Cover Letter: # PATH TO cover letter + Photo: # PATH TO photo output_filename: - # PATH TO OUTPUT FILE (default output.csv) @@ -37,6 +39,11 @@ blacklist: ``` __NOTE: AFTER EDITING SAVE FILE, DO NOT COMMIT FILE__ +### Uploads + +There is no limit to the number of files you can list in the uploads section. +The program takes the titles from the input boxes and tries to match them with +list in the config file. ## Execute diff --git a/config.yaml b/config.yaml index 655292f..af5b02b 100644 --- a/config.yaml +++ b/config.yaml @@ -2,19 +2,23 @@ username: password: positions: -- # positions you want to search for +- Data Scientist - # Another position you want to search for - # A third position you want to search for locations: -- # Location you want to search for +- Remote - # A second location you want to search in -cover_letter_loctn: -- # '/home/PATH_TO_FILE' +# --------- Optional Parameters ------- +# uploads: +# Resume: # PATH TO Resume +# Cover Letter: # PATH TO cover letter +# Photo: # PATH TO photo -output_filename: -- # PATH TO OUTPUT FILE (default output.csv) -blacklist: -- # Company names you want to ignore \ No newline at end of file +# output_filename: +# - # PATH TO OUTPUT FILE (default output.csv) + +# blacklist: +# - # Company names you want to ignore \ No newline at end of file diff --git a/easyapplybot.py b/easyapplybot.py index e61b797..5e93cbf 100644 --- a/easyapplybot.py +++ b/easyapplybot.py @@ -31,12 +31,10 @@ class EasyApplyBot: - MAX_SEARCH_TIME = 10*60 - def __init__(self, username, password, - cover_letter_loctn=None, + uploads={}, filename='output.csv', blacklist=[]): @@ -44,8 +42,9 @@ def __init__(self, dirpath = os.getcwd() log.info("current directory is : " + dirpath) - self.cover_letter_loctn = cover_letter_loctn - self.appliedJobIDs = self.get_appliedIDs(filename) if self.get_appliedIDs(filename) != None else [] + self.uploads = uploads + past_ids = self.get_appliedIDs(filename) + self.appliedJobIDs = past_ids if past_ids != None else [] self.filename = filename self.options = self.browser_options() self.browser = driver @@ -55,13 +54,12 @@ def __init__(self, def get_appliedIDs(self, filename): - print(filename) try: df = pd.read_csv(filename, header=None, names=['timestamp', 'jobID', 'job', 'company', 'attempted', 'result'], lineterminator='\n', - encoding = 'utf-8') + encoding='utf-8') df['timestamp'] = pd.to_datetime(df['timestamp'], format="%Y-%m-%d %H:%M:%S.%f") df = df[df['timestamp'] > (datetime.now() - timedelta(days=2))] @@ -101,13 +99,11 @@ def start_linkedin(self,username,password): except TimeoutException: log.info("TimeoutException! Username/password field or login button not found") - def fill_data(self): self.browser.set_window_size(0, 0) self.browser.set_window_position(2000, 2000) - def start_apply(self, positions, locations): start = time.time() self.fill_data() @@ -176,7 +172,6 @@ def applications_loop(self, position, location): jobIDs = [x for x in IDs if x not in self.appliedJobIDs] after = len(jobIDs) - if len(jobIDs) == 0 and len(IDs) > 24: jobs_per_page = jobs_per_page + 25 count_job = 0 @@ -265,10 +260,9 @@ def re_extract(text, pattern): def get_job_page(self, jobID): - #root = 'www.linkedin.com' - #if root not in job: - job = 'https://www.linkedin.com/jobs/view/'+ str(jobID) + '/' - log.info("Opening Job Page \n %s", job) + + job = 'https://www.linkedin.com/jobs/view/'+ str(jobID) + self.browser.get(job) self.job_page = self.load_page(sleep=0.5) return job, self.job_page @@ -299,6 +293,7 @@ def is_present(button_locator): return (len(self.browser.find_elements(button_locator[0], button_locator[1])) > 0) try: + time.sleep(3) log.info("Attempting to send resume") #TODO These locators are not future proof. These labels could easily change. Ideally we would search for contained text; @@ -311,6 +306,7 @@ def is_present(button_locator): error_locator = (By.CSS_SELECTOR, "p[data-test-form-element-error-message='true']") cover_letter = (By.CSS_SELECTOR, "input[name='file']") + testLabel_locator = (By.XPATH, "//span[@data-test-form-element-label-title='true']") yes_locator = (By.XPATH, "//input[@value='Yes']") no_locator = (By.XPATH, "//input[@value='No']") @@ -323,11 +319,22 @@ def is_present(button_locator): button = None # Upload Cover Letter if possible - if is_present(cover_letter): - input_button = self.browser.find_elements(cover_letter[0], - cover_letter[1]) - #TODO is this cover letter the same thing as the resume upload locator? - input_button[0].send_keys(self.cover_letter_loctn) + + if is_present(upload_locator): + + input_buttons = self.browser.find_elements(upload_locator[0], + upload_locator[1]) + for input_button in input_buttons: + parent = input_button.find_element(By.XPATH, "..") + sibling = parent.find_element(By.XPATH, "preceding-sibling::*") + grandparent = sibling.find_element(By.XPATH, "..") + for key in self.uploads.keys(): + if key in sibling.text or key in grandparent.text: + input_button.send_keys(self.uploads[key]) + + + #input_button[0].send_keys(self.cover_letter_loctn) + time.sleep(random.uniform(4.5, 6.5)) for i, button_locator in enumerate( @@ -511,6 +518,7 @@ def next_jobs_page(self, position, location, jobs_per_page): def finish_apply(self): self.browser.close() + def setupLogger(): dt = datetime.strftime(datetime.now(), "%m_%d_%y %H_%M_%S ") @@ -526,6 +534,7 @@ def setupLogger(): c_handler.setFormatter(c_format) log.addHandler(c_handler) + if __name__ == '__main__': setupLogger() @@ -543,22 +552,18 @@ def setupLogger(): print(parameters) - resume_loctn = parameters.get('resume_loctn') - cover_letter_loctn = parameters.get('cover_letter_loctn') - output_filename = parameters.get('output_filename') - blacklist = parameters.get('blacklist') - - #default to output file if nothing was given. - if output_filename == [None]: - output_filename = "./output.csv" - if not os.path.exists(output_filename): - with open(output_filename, 'w+') as f: - writer = csv.writer(f) - writer.writerow(['DateTime', 'JobID', 'Title', 'Company', 'Attempted', 'Success']) + + output_filename = [f for f in parameters.get('output_filename', ['output.csv']) if f != None] + output_filename = output_filename[0] if len(output_filename) > 0 else 'output.csv' + blacklist = parameters.get('blacklist', []) + uploads = parameters.get('uploads', {}) + for key in uploads.keys(): + assert uploads[key] != None + bot = EasyApplyBot(parameters['username'], parameters['password'], - cover_letter_loctn=cover_letter_loctn, + uploads=uploads, filename=output_filename, blacklist=blacklist )