From f948443a3975243edf5c8922346fa6c7a43cfa5b Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Thu, 29 Aug 2024 17:08:26 +0200 Subject: [PATCH 1/6] Make the function better, because is not needed check selectors. when a user is logged on linkedin will be redirected already on /feed/ endpoint, otherwise url will remain https://linkedin.com, so is possible check the state of login just using this approach --- src/linkedIn_authenticator.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/linkedIn_authenticator.py b/src/linkedIn_authenticator.py index c953e5a..0153504 100644 --- a/src/linkedIn_authenticator.py +++ b/src/linkedIn_authenticator.py @@ -65,18 +65,8 @@ class LinkedInAuthenticator: print("Security check not completed. Please try again later.") def is_logged_in(self): - self.driver.get('https://www.linkedin.com/feed') - try: - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, 'share-box-feed-entry__trigger')) - ) - buttons = self.driver.find_elements(By.CLASS_NAME, 'share-box-feed-entry__trigger') - if any(button.text.strip() == 'Start a post' for button in buttons): - print("User is already logged in.") - return True - except TimeoutException: - pass - return False + self.driver.get('https://www.linkedin.com/') + return self.driver.current_url == 'https://www.linkedin.com/feed/' def wait_for_page_load(self, timeout=10): try: From e898df98808d44fc36abb9eecd78f52ad309ed38 Mon Sep 17 00:00:00 2001 From: feder-cr <85809106+feder-cr@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:11:38 +0200 Subject: [PATCH 2/6] v3 lib --- requirements.txt | Bin 670 -> 680 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index f74a689c00362ab6724bec113a729791b031cce0..ac9bc87c6e40b017241fe7176e70bd89834eb044 100644 GIT binary patch delta 18 ZcmbQox`K7XJSHv&hB5|Y23`g(1^_T51FHZ4 delta 7 OcmZ3%I*)b3JSG4OH3Fdk From 0e90d4be7dbcf67e61b1935ce8d30251d7c7652a Mon Sep 17 00:00:00 2001 From: feder-cr <85809106+feder-cr@users.noreply.github.com> Date: Thu, 29 Aug 2024 19:01:42 +0200 Subject: [PATCH 3/6] now we use pydantic for yaml validator --- requirements.txt | Bin 680 -> 674 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index ac9bc87c6e40b017241fe7176e70bd89834eb044..aef4baed9ae147969488a70f26fd5aae9edacb36 100644 GIT binary patch delta 12 TcmZ3%x`=hdJSG-i1}+8w7~TTF delta 18 ZcmZ3)x`K7XJSHv&hB5|Y23`g(1^_T@1Frx8 From 761bb91e96c6fd35c64151743e510db0c85d7855 Mon Sep 17 00:00:00 2001 From: 1 Date: Sat, 31 Aug 2024 12:41:05 +0300 Subject: [PATCH 4/6] Readme upd to ease troubleshooting and fixing --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3258335..15fe7b8 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ LinkedIn_AIHawk steps in as a game-changing solution to these challenges. It's n ## Installation **Please watch this video to set up your LinkedIn_AIHawk: [How to set up LinkedIn_AIHawk](https://youtu.be/gdW9wogHEUM) - https://youtu.be/gdW9wogHEUM** - +0. **Confirmed succesfull runs OSs & Python**: Python 3.10, 3.11.9(64b), 3.12.5(64b) . Windows 10, Ubuntu 22 1. **Download and Install Python:** Ensure you have the last Python version installed. If not, download and install it from Python's official website. For detailed instructions, refer to the tutorials: @@ -507,11 +507,16 @@ TODO ): ## Troubleshooting +- **Carefully read logs and output :** Most of the errors are verbosely reflected just watch the output and try to find the root couse. +- **If nothing works by unknown reason:** Use tested OS. Reboot and/or update OS. Use new clean venv. Try update Python to the tested version. - **ChromeDriver Issues:** Ensure ChromeDriver is compatible with your installed Chrome version. - **Missing Files:** Verify that all necessary files are present in the data folder. -- **Invalid YAML:** Check your YAML files for syntax errors. - - If you encounter any issues, you can open an issue on [GitHub](https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/issues). I'll be more than happy to assist you! +- **Invalid YAML:** Check your YAML files for syntax errors . Try to use external YAML validators e.g. https://www.yamllint.com/ +- **OpenAI endpoint isues**: Try to check possible limits\blocking at their side + +If you encounter any issues, you can open an issue on [GitHub](https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/issues). + Please add valuable details to the subject and to the description. If you need new feature then please reflect this. + I'll be more than happy to assist you! ## Conclusion From 9fc3274b9afe23dad1294612db8226b236e8bf26 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 31 Aug 2024 19:31:31 +0200 Subject: [PATCH 5/6] Adding linkedin-api.py to search jobs without use selenium. --- src/linkedin-api.py | 168 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/linkedin-api.py diff --git a/src/linkedin-api.py b/src/linkedin-api.py new file mode 100644 index 0000000..f1395f8 --- /dev/null +++ b/src/linkedin-api.py @@ -0,0 +1,168 @@ +from typing import Dict, List +from linkedin_api import Linkedin +from typing import Optional, Union, Literal +from urllib.parse import urlencode + +class LinkedInEvolvedAPI(Linkedin): + def __init__(self, username, password): + super().__init__(username, password) + + def search_jobs( + self, + keywords: Optional[str] = None, + companies: Optional[List[str]] = None, + experience: Optional[ + List[ + Union[ + Literal["1"], + Literal["2"], + Literal["3"], + Literal["4"], + Literal["5"], + Literal["6"], + ] + ] + ] = None, + job_type: Optional[ + List[ + Union[ + Literal["F"], + Literal["C"], + Literal["P"], + Literal["T"], + Literal["I"], + Literal["V"], + Literal["O"], + ] + ] + ] = None, + job_title: Optional[List[str]] = None, + industries: Optional[List[str]] = None, + location_name: Optional[str] = None, + remote: Optional[List[Union[Literal["1"], Literal["2"], Literal["3"]]]] = None, + listed_at=24 * 60 * 60, + distance: Optional[int] = None, + easy_apply: Optional[bool] = True, + limit=-1, + offset=0, + **kwargs, + ) -> List[Dict]: + """Perform a LinkedIn search for jobs. + + :param keywords: Search keywords (str) + :type keywords: str, optional + :param companies: A list of company URN IDs (str) + :type companies: list, optional + :param experience: A list of experience levels, one or many of "1", "2", "3", "4", "5" and "6" (internship, entry level, associate, mid-senior level, director and executive, respectively) + :type experience: list, optional + :param job_type: A list of job types , one or many of "F", "C", "P", "T", "I", "V", "O" (full-time, contract, part-time, temporary, internship, volunteer and "other", respectively) + :type job_type: list, optional + :param job_title: A list of title URN IDs (str) + :type job_title: list, optional + :param industries: A list of industry URN IDs (str) + :type industries: list, optional + :param location_name: Name of the location to search within. Example: "Kyiv City, Ukraine" + :type location_name: str, optional + :param remote: Filter for remote jobs, onsite or hybrid. onsite:"1", remote:"2", hybrid:"3" + :type remote: list, optional + :param listed_at: maximum number of seconds passed since job posting. 86400 will filter job postings posted in last 24 hours. + :type listed_at: int/str, optional. Default value is equal to 24 hours. + :param distance: maximum distance from location in miles + :type distance: int/str, optional. If not specified, None or 0, the default value of 25 miles applied. + :param easy_apply: filter for jobs that are easy to apply to + :type easy_apply: bool, optional. Default value is True. + :param limit: maximum number of results obtained from API queries. -1 means maximum which is defined by constants and is equal to 1000 now. + :type limit: int, optional, default -1 + :param offset: indicates how many search results shall be skipped + :type offset: int, optional + :return: List of jobs + :rtype: list + """ + count = Linkedin._MAX_SEARCH_COUNT + if limit is None: + limit = -1 + + query: Dict[str, Union[str, Dict[str, str]]] = { + "origin": "JOB_SEARCH_PAGE_QUERY_EXPANSION" + } + if keywords: + query["keywords"] = "KEYWORD_PLACEHOLDER" + if location_name: + query["locationFallback"] = "LOCATION_PLACEHOLDER" + + query["selectedFilters"] = {} + if companies: + query["selectedFilters"]["company"] = f"List({','.join(companies)})" + if experience: + query["selectedFilters"]["experience"] = f"List({','.join(experience)})" + if job_type: + query["selectedFilters"]["jobType"] = f"List({','.join(job_type)})" + if job_title: + query["selectedFilters"]["title"] = f"List({','.join(job_title)})" + if industries: + query["selectedFilters"]["industry"] = f"List({','.join(industries)})" + if distance: + query["selectedFilters"]["distance"] = f"List({distance})" + if remote: + query["selectedFilters"]["workplaceType"] = f"List({','.join(remote)})" + if easy_apply: + query["selectedFilters"]["easyApply"] = "List(true)" + + query["selectedFilters"]["timePostedRange"] = f"List(r{listed_at})" + query["spellCorrectionEnabled"] = "true" + + query_string = ( + str(query) + .replace(" ", "") + .replace("'", "") + .replace("KEYWORD_PLACEHOLDER", keywords or "") + .replace("LOCATION_PLACEHOLDER", location_name or "") + .replace("{", "(") + .replace("}", ")") + ) + results = [] + while True: + if limit > -1 and limit - len(results) < count: + count = limit - len(results) + default_params = { + "decorationId": "com.linkedin.voyager.dash.deco.jobs.search.JobSearchCardsCollection-174", + "count": count, + "q": "jobSearch", + "query": query_string, + "start": len(results) + offset, + } + + res = self._fetch( + f"/voyagerJobsDashJobCards?{urlencode(default_params, safe='(),:')}", + headers={"accept": "application/vnd.linkedin.normalized+json+2.1"}, + ) + data = res.json() + + elements = data.get("included", []) + new_data = [] + for e in elements: + trackingUrn = e.get("trackingUrn") + if trackingUrn: + trackingUrn = trackingUrn.split(":")[-1] + e["job_id"] = trackingUrn + if e.get("$type") == "com.linkedin.voyager.dash.jobs.JobPosting": + new_data.append(e) + + if not new_data: + break + results.extend(new_data) + if ( + (-1 < limit <= len(results)) + or len(results) / count >= Linkedin._MAX_REPEATED_REQUESTS + ) or len(elements) == 0: + break + + self.logger.debug(f"results grew to {len(results)}") + + return results + + + + + + \ No newline at end of file From bac05ad04cbae9bfe9429aa8b2b0e403cc18b08d Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 31 Aug 2024 19:39:44 +0200 Subject: [PATCH 6/6] Adding linkedin-api.py to search jobs without use selenium (requirements) --- requirements.txt | Bin 674 -> 698 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index aef4baed9ae147969488a70f26fd5aae9edacb36..341ed8e0d589527317d1b1884894f4ad7e2a7733 100644 GIT binary patch delta 32 jcmZ3)x{GzgA|{C(hD?S$hHQpZh7>SMmm!g%0LTIWkiG|q delta 7 OcmdnRx`=hdA|?O}&jPyu