Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Firefox browser compatibility #707

Merged
merged 22 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9283f37
feat: add support for Firefox browser compatibility
Tverous Nov 2, 2024
8eb24ac
refactor: implement factory pattern for browsers
Tverous Nov 4, 2024
081ae47
refactor: remove duplicates
Tverous Nov 4, 2024
227875f
refactor: remote duplicates and change the browser type names
Tverous Nov 4, 2024
f2f32fb
resolve conflicts
Tverous Nov 8, 2024
0ec4325
remove duplicates
Tverous Nov 8, 2024
e400a25
fix: package path
Tverous Nov 8, 2024
65dc6b4
refactor: move the browser config to config.py instead
Tverous Nov 8, 2024
7a4f4c7
update unit test cases and remove duplicates
Tverous Nov 8, 2024
767b073
Merge branch 'release/v11.15.2024' into firefox-patch
surapuramakhil Nov 11, 2024
03f387f
test: update test cases
Tverous Nov 12, 2024
af7271a
refactor: simplify the browser factory
Tverous Nov 12, 2024
74ac86e
refactor: remove unused package
Tverous Nov 12, 2024
e524214
refactor: browser factory
Tverous Nov 12, 2024
5de4d16
refactor: remove duplicates and using singleton
Tverous Nov 14, 2024
8b30c74
Merge remote-tracking branch 'upstream/release/v11.15.2024' into fire…
Tverous Nov 14, 2024
f289166
Merge branch 'release/v3.1.0' into firefox-patch
surapuramakhil Nov 14, 2024
dc875e6
Update browser_factory.py
surapuramakhil Nov 15, 2024
bca071f
refactor: browser type
Tverous Nov 16, 2024
c160ece
Merge branch 'release/v4.1.0' into firefox-patch
surapuramakhil Nov 19, 2024
84e3cea
Merge branch 'release/v4.1.0' into firefox-patch
surapuramakhil Dec 2, 2024
19b36c1
test code changes, fixed deprication issue
surapuramakhil Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# In this file, you can set the configurations of the app.

from constants import DEBUG, LLM_MODEL, OPENAI
from src.webdrivers.browser_type import BrowserType

#config related to logging must have prefix LOG_
LOG_LEVEL = DEBUG
Expand All @@ -12,6 +13,8 @@

JOB_APPLICATIONS_DIR = "job_applications"
JOB_SUITABILITY_SCORE = 7
BROWSER_TYPE_CONFIG = BrowserType.CHROME


JOB_MAX_APPLICATIONS = 5
JOB_MIN_APPLICATIONS = 1
Expand Down
6 changes: 1 addition & 5 deletions data_folder/work_preferences.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,4 @@ title_blacklist:
- word2

location_blacklist:
- Brazil

job_applicants_threshold:
min_applicants: 0
max_applicants: 30
- Brazil
17 changes: 3 additions & 14 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
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
from selenium.common.exceptions import WebDriverException
from lib_resume_builder_AIHawk import Resume, FacadeManager, ResumeGenerator, StyleManager
from typing import Optional
from constants import PLAIN_TEXT_RESUME_YAML, SECRETS_YAML, WORK_PREFERENCES_YAML
from src.utils.chrome_utils import chrome_browser_options

from src.webdrivers.browser_factory import BrowserFactory
from src.job_application_profile import JobApplicationProfile
from src.logging import logger

Expand Down Expand Up @@ -155,14 +152,6 @@ def file_paths_to_dict(resume_file: Path | None, plain_text_resume_file: Path) -

return result

def init_browser() -> webdriver.Chrome:
try:
options = chrome_browser_options()
service = ChromeService(ChromeDriverManager().install())
return webdriver.Chrome(service=service, options=options)
except Exception as e:
raise RuntimeError(f"Failed to initialize browser: {str(e)}")

def create_and_run_bot(parameters, llm_api_key):
try:
style_manager = StyleManager()
Expand All @@ -178,7 +167,7 @@ def create_and_run_bot(parameters, llm_api_key):

job_application_profile_object = JobApplicationProfile(plain_text_resume)

browser = init_browser()
browser = BrowserFactory.get_browser()
login_component = get_authenticator(driver=browser, platform='linkedin')
apply_component = AIHawkJobManager(browser)
gpt_answerer_component = GPTAnswerer(parameters, llm_api_key)
Expand Down Expand Up @@ -213,7 +202,7 @@ def main(collect: bool = False, resume: Optional[Path] = None):
parameters['uploads'] = FileManager.file_paths_to_dict(resume, plain_text_resume_file)
parameters['outputFileDirectory'] = output_folder
parameters['collectMode'] = collect

create_and_run_bot(parameters, llm_api_key)
except ConfigError as ce:
logger.error(f"Configuration error: {str(ce)}")
Expand Down
1 change: 0 additions & 1 deletion src/ai_hawk/job_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import time
from itertools import product
from pathlib import Path
from turtle import color

from inputimeout import inputimeout, TimeoutOccurred
from selenium.common.exceptions import NoSuchElementException
Expand Down
60 changes: 0 additions & 60 deletions src/utils/chrome_utils.py

This file was deleted.

67 changes: 67 additions & 0 deletions src/webdrivers/base_browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os

from abc import ABC, abstractmethod
from loguru import logger


class BrowserProfile:
"""Manages browser profile creation and configuration"""
def __init__(self, browser_type: str):
self.browser_type: str = browser_type.lower()
self.profile_path = os.path.join(
os.getcwd(),
f"{self.browser_type}_profile",
"linkedin_profile"
)

def ensure_profile_exists(self) -> str:
"""
Ensures the browser profile directory exists
Returns: Path to the profile directory
"""
logger.debug(f"Ensuring {self.browser_type} profile exists at path: {self.profile_path}")
profile_dir = os.path.dirname(self.profile_path)

if not os.path.exists(profile_dir):
os.makedirs(profile_dir)
logger.debug(f"Created directory for {self.browser_type} profile: {profile_dir}")

if not os.path.exists(self.profile_path):
os.makedirs(self.profile_path)
logger.debug(f"Created {self.browser_type} profile directory: {self.profile_path}")

return self.profile_path

class Browser(ABC):
"""Abstract base class for browser implementations"""
def __init__(self):
self.profile = BrowserProfile(self.browser_type)

@property
def browser_type(self) -> str:
"""Return the browser type identifier"""
return self.__class__.browser_type

@abstractmethod
def create_options(self):
"""Create and return browser-specific options"""

@abstractmethod
def create_service(self):
"""Create and return browser-specific service"""

def create_driver(self):
"""Create and return browser-specific WebDriver instance"""
try:
options = self.create_options()
service = self.create_service()
driver = self._create_driver_instance(service, options)
logger.debug(f"{self.browser_type} WebDriver instance created successfully")
return driver
except Exception as e:
logger.error(f"Failed to create {self.browser_type} WebDriver: {e}")
raise RuntimeError(f"Failed to initialize {self.browser_type} browser: {str(e)}")

@abstractmethod
def _create_driver_instance(self, service, options):
"""Create the specific driver instance"""
43 changes: 43 additions & 0 deletions src/webdrivers/browser_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Union

from selenium import webdriver
from loguru import logger

from config import BROWSER_TYPE_CONFIG
from src.webdrivers.browser_type import BrowserType


class BrowserFactory:
"""Factory class for creating browser instances"""
_browser_type: BrowserType = BROWSER_TYPE_CONFIG
@classmethod
def get_browser_type(cls) -> BrowserType:
"""Get current browser type"""
return cls._browser_type

@classmethod
def set_browser_type(cls, browser_type: BrowserType) -> None:
"""Set browser type"""
# safety check additional to type check.
if browser_type not in BrowserType:
raise ValueError(f"Unsupported browser type: {browser_type}")
cls._browser_type = browser_type
logger.debug(f"Browser type set to: {browser_type}")

@classmethod
def get_browser(cls) -> Union[webdriver.Chrome, webdriver.Firefox]:
"""
Create and return a WebDriver instance for the specified browser type
Args:
browser_type: BrowserType enum value
Returns:
WebDriver instance
Raises:
RuntimeError: If browser initialization fails
"""
if cls._browser_type not in BrowserType:
raise ValueError("Unsupported browser type: {cls._browser_type}")

browser = cls._browser_type.value()

return browser.create_driver()
10 changes: 10 additions & 0 deletions src/webdrivers/browser_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from enum import Enum

from src.webdrivers.chrome import Chrome
from src.webdrivers.firefox import Firefox


class BrowserType(Enum):
"""Enum for supported browser types"""
CHROME = Chrome
FIREFOX = Firefox
59 changes: 59 additions & 0 deletions src/webdrivers/chrome.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os

from loguru import logger
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager

from src.webdrivers.base_browser import Browser


class Chrome(Browser):
"""Chrome browser implementation"""
browser_type: str = "chrome"

def create_options(self) -> webdriver.ChromeOptions:
"""Create Chrome-specific options"""
self.profile.ensure_profile_exists()
options = webdriver.ChromeOptions()

chrome_arguments = [
"--start-maximized", "--no-sandbox", "--disable-dev-shm-usage",
"--ignore-certificate-errors", "--disable-extensions", "--disable-gpu",
"window-size=1200x800", "--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows", "--disable-translate",
"--disable-popup-blocking", "--no-first-run", "--no-default-browser-check",
"--disable-logging", "--disable-autofill", "--disable-plugins",
"--disable-animations", "--disable-cache"
]

for arg in chrome_arguments:
options.add_argument(arg)

options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"])

prefs = {
"profile.default_content_setting_values.images": 2,
"profile.managed_default_content_settings.stylesheets": 2,
}
options.add_experimental_option("prefs", prefs)

if self.profile.profile_path:
initial_path = os.path.dirname(self.profile.profile_path)
profile_dir = os.path.basename(self.profile.profile_path)
options.add_argument('--user-data-dir=' + initial_path)
options.add_argument("--profile-directory=" + profile_dir)
logger.debug(f"Using Chrome profile directory: {self.profile.profile_path}")
else:
options.add_argument("--incognito")
logger.debug("Using Chrome in incognito mode")

return options

def create_service(self) -> ChromeService:
"""Create Chrome-specific service"""
return ChromeService(ChromeDriverManager().install())

def _create_driver_instance(self, service, options):
"""Create Chrome WebDriver instance"""
return webdriver.Chrome(service=service, options=options)
54 changes: 54 additions & 0 deletions src/webdrivers/firefox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from loguru import logger
from selenium import webdriver
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.firefox import GeckoDriverManager

from src.webdrivers.base_browser import Browser


class Firefox(Browser):
"""Firefox browser implementation"""
browser_type: str = "firefox"

def create_options(self) -> webdriver.FirefoxOptions:
"""Create Firefox-specific options"""
self.profile.ensure_profile_exists()
options = webdriver.FirefoxOptions()

firefox_arguments = [
"--start-maximized", "--no-sandbox", "--disable-dev-shm-usage",
"--ignore-certificate-errors", "--disable-extensions", "--disable-gpu",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows", "--disable-translate",
"--disable-popup-blocking", "--no-first-run", "--no-default-browser-check",
"--disable-logging", "--disable-autofill", "--disable-plugins",
"--disable-animations", "--disable-cache"
]

for arg in firefox_arguments:
options.add_argument(arg)

prefs = {
"permissions.default.image": 2,
"permissions.default.stylesheet": 2,
}
for key, value in prefs.items():
options.set_preference(key, value)

if self.profile.profile_path:
profile = webdriver.FirefoxProfile(self.profile.profile_path)
options.profile = profile
logger.debug(f"Using Firefox profile directory: {self.profile.profile_path}")
else:
options.set_preference("browser.privatebrowsing.autostart", True)
logger.debug("Using Firefox in private browsing mode")

return options

def create_service(self) -> FirefoxService:
"""Create Firefox-specific service"""
return FirefoxService(GeckoDriverManager().install())

def _create_driver_instance(self, service, options):
"""Create Firefox WebDriver instance"""
return webdriver.Firefox(service=service, options=options)
Loading