Skip to content

PatchWork GenerateREADME #1560

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,10 @@ benchmark/

# node modules
node_modules/

tmp/

# Key files
*.pem
*.key
*.gpg
8 changes: 5 additions & 3 deletions patchwork/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,20 +144,22 @@ def sigint_handler(signum, frame):
@click.option("patched_api_key", "--patched_api_key", help="API key to use with the patched.codes service.")
@click.option("disable_telemetry", "--disable_telemetry", is_flag=True, help="Disable telemetry.", default=False)
@click.option("debug", "--debug", is_flag=True, help="Enable debug mode.", default=False)
@click.option("plain", "--plain", is_flag=True, help="Enable plain mode (no panel).", default=False)
def cli(
log: str,
patchflow: str,
opts: list[str],
config: str | None,
output: str | None,
plain: bool,
data_format: str,
patched_api_key: str | None,
disable_telemetry: bool,
debug: bool,
):
setup_cli()

init_cli_logger(log)
init_cli_logger(log, plain)

if "::" in patchflow:
module_path, _, patchflow_name = patchflow.partition("::")
Expand All @@ -167,7 +169,7 @@ def cli(

possbile_module_paths = deque((module_path,))

panel = logger.panel("Initializing Patchwork CLI") if debug else nullcontext()
panel = nullcontext() if plain or not debug else logger.panel("Initializing Patchwork CLI")

with panel:
inputs = {}
Expand Down Expand Up @@ -227,7 +229,7 @@ def cli(
# treat --key=value as a key-value pair
inputs[key] = value

patchflow_panel = nullcontext() if debug else logger.panel(f"Patchflow {patchflow} inputs")
patchflow_panel = nullcontext() if plain or not debug else logger.panel(f"Patchflow {patchflow} inputs")

with patchflow_panel as _:
if debug is True:
Expand Down
167 changes: 167 additions & 0 deletions patchwork/common/utils/browser_initializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import logging
import os
from typing import List, Optional

from browser_use import Browser, BrowserConfig, BrowserContextConfig, Controller
from browser_use.agent.views import ActionResult
from browser_use.browser.context import BrowserContext

logger = logging.getLogger(__name__)


async def set_file_input(index: int, paths: str | List[str], browser: BrowserContext):
"""
Set the file input value to the given path or list of paths.

Args:
index: The DOM element index to target
paths: Local file path or list of local file paths to upload
browser: Browser context for interaction

Returns:
ActionResult: Result of the upload operation
"""
if isinstance(paths, str):
paths = [paths]

for path in paths:
if not os.path.exists(path):
return ActionResult(error=f"File {path} does not exist")

dom_el = await browser.get_dom_element_by_index(index)
file_upload_dom_el = dom_el.get_file_upload_element()

if file_upload_dom_el is None:
msg = f"No file upload element found at index {index}. The element may be hidden or not an input type file"
logger.info(msg)
return ActionResult(error=msg)

file_upload_el = await browser.get_locate_element(file_upload_dom_el)

if file_upload_el is None:
msg = f"No file upload element found at index {index}. The element may be hidden or not an input type file"
logger.info(msg)
return ActionResult(error=msg)

try:
await file_upload_el.set_input_files(paths)
msg = f"Successfully set file input value to {paths}"
logger.info(msg)
return ActionResult(extracted_content=msg, include_in_memory=True)
except Exception as e:
msg = f"Failed to upload file to index {index}: {str(e)}"
logger.info(msg)
return ActionResult(error=msg)


async def close_current_tab(browser: BrowserContext):
await browser.close_current_tab()
msg = "🔄 Closed current tab"
logger.info(msg)
return ActionResult(extracted_content=msg, include_in_memory=True)


class BrowserInitializer:
"""
Initialize and cache browser and controller instances.

This class uses a singleton pattern to ensure we only create one browser
instance throughout the application lifecycle, which saves resources.
"""

_browser = None
_controller = None
_browser_context = None

@classmethod
def init_browser(cls, config=BrowserConfig()):
"""
Initialize and cache the Browser instance.

Returns:
Browser: Browser instance for web automation
"""
if cls._browser is not None:
return cls._browser

cls._browser = Browser(config=config)
return cls._browser

@classmethod
def init_browser_context(cls, config: Optional[BrowserConfig], downloads_path: Optional[str] = None):
"""
Initialize and cache the BrowserContext instance.

Returns:
BrowserContext: BrowserContext instance for managing browser context
"""
if cls._browser_context is not None:
return cls._browser_context

if downloads_path and not os.path.exists(downloads_path):
os.makedirs(downloads_path)

context_config = BrowserContextConfig(
# cookies_file=cookies_file,
browser_window_size={"width": 1920, "height": 1080},
)
browser = cls.init_browser(config=config)

class BrowserContextWithDownloadHandling(BrowserContext):
async def handle_download(self, download):
suggested_filename = download.suggested_filename
unique_filename = await self._get_unique_filename(downloads_path, suggested_filename)
download_path = os.path.join(downloads_path, unique_filename)
await download.save_as(download_path)
logger.info(f"Downloaded file saved to {download_path}")

async def _initialize_session(self):
async def _download_listener(download):
logger.info("[BUD] Download event triggered")
await self.handle_download(download)
return download

def _new_page_listener(page):
logger.info("[BUD] Adding download event listener to page")
page.on("download", _download_listener)
return page

await super()._initialize_session()

logger.info("[BUD] Adding page event listener to context")
self.session.context.on("page", _new_page_listener)

logger.info(f"[BUD] Adding download event listener to {len(self.session.context.pages)} existing pages")
for page in self.session.context.pages:
page.on("download", _download_listener)

cls._browser_context = (
BrowserContextWithDownloadHandling(browser=browser, config=context_config)
if downloads_path
else BrowserContext(browser=browser, config=context_config)
)
return cls._browser_context

@classmethod
def init_controller(cls):
"""
Initialize and cache the Controller instance.

Returns:
Controller: Controller instance for managing browser actions
"""
if cls._controller is not None:
return cls._controller

controller = Controller()

controller.action(
"Set the value of a file input to the given path or list of paths",
)(set_file_input)

controller.action(
description="Close the tab that is currently active",
)(close_current_tab)

cls._controller = controller
return cls._controller
47 changes: 26 additions & 21 deletions patchwork/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from patchwork.managed_files import HOME_FOLDER, LOG_FILE

# Create a global console object
console = Console()
console = Console(force_terminal=True, no_color=False)

# Add TRACE level to logging
logging.TRACE = logging.DEBUG - 1
Expand All @@ -39,10 +39,11 @@ def evict_null_handler():


class TerminalHandler(RichHandler):
def __init__(self, log_level: str):
def __init__(self, log_level: str, plain: bool):
self.plain = plain
super().__init__(
console=console,
rich_tracebacks=True,
rich_tracebacks=not plain,
tracebacks_suppress=[click],
show_time=False,
show_path=False,
Expand Down Expand Up @@ -95,22 +96,25 @@ def deregister_progress_bar(self):
@contextlib.contextmanager
def panel(self, title: str):
global console
self.__panel_lines = []
self.__panel_title = title
self.__panel = Panel("", title=title)
renderables = [self.__panel]
if self.__progress_bar is not None:
renderables.append(self.__progress_bar)

self.__live = Live(Group(*renderables), console=console, vertical_overflow="visible")
try:
self.__live.start()
if self.plain:
yield
except Exception as e:
raise e
finally:
self.__reset_live()
self.console.print("\n")
else:
self.__panel_lines = []
self.__panel_title = title
self.__panel = Panel("", title=title)
renderables = [self.__panel]
if self.__progress_bar is not None:
renderables.append(self.__progress_bar)

self.__live = Live(Group(*renderables), console=console, vertical_overflow="visible")
try:
self.__live.start()
yield
except Exception as e:
raise e
finally:
self.__reset_live()
self.console.print("\n")

def emit(self, record: logging.LogRecord) -> None:
markup = getattr(record, "markup", None)
Expand All @@ -126,7 +130,8 @@ def emit(self, record: logging.LogRecord) -> None:
if self.__panel is not None:
self.__emit_panel(record)
else:
setattr(record, "markup", True)
if not self.plain:
setattr(record, "markup", True)
super().emit(record)

def __emit_panel(self, record: logging.LogRecord) -> None:
Expand All @@ -143,7 +148,7 @@ def inner(record: logging.LogRecord) -> bool:
return inner


def init_cli_logger(log_level: str) -> logging.Logger:
def init_cli_logger(log_level: str, plain: bool) -> logging.Logger:
global logger

evict_null_handler()
Expand All @@ -158,7 +163,7 @@ def init_cli_logger(log_level: str) -> logging.Logger:
except FileNotFoundError:
logger.error(f"Unable to create log file: {LOG_FILE}")

th = TerminalHandler(log_level.upper())
th = TerminalHandler(log_level.upper(), plain)
logger.addHandler(th)
setattr(logger, "panel", th.panel)
setattr(logger, "register_progress_bar", th.register_progress_bar)
Expand Down
Loading