Skip to content

Commit

Permalink
Merge pull request #707 from Tverous/firefox-patch
Browse files Browse the repository at this point in the history
feat: add support for Firefox browser compatibility
  • Loading branch information
surapuramakhil authored Dec 2, 2024
2 parents cc0b814 + 19b36c1 commit f1b88f6
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 81 deletions.
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
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: 1 addition & 0 deletions src/ai_hawk/job_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from datetime import datetime


from inputimeout import inputimeout, TimeoutOccurred
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
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)
53 changes: 53 additions & 0 deletions src/webdrivers/firefox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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:
options.set_preference("profile", self.profile.profile_path)
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

0 comments on commit f1b88f6

Please sign in to comment.