diff --git a/.gitignore b/.gitignore index f26ec42d3..aff958aec 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,10 @@ benchmark/ # node modules node_modules/ + +tmp/ + +# Key files +*.pem +*.key +*.gpg diff --git a/patchwork/app.py b/patchwork/app.py index 4e33609e7..4f1f64ae9 100644 --- a/patchwork/app.py +++ b/patchwork/app.py @@ -144,12 +144,14 @@ 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, @@ -157,7 +159,7 @@ def cli( ): setup_cli() - init_cli_logger(log) + init_cli_logger(log, plain) if "::" in patchflow: module_path, _, patchflow_name = patchflow.partition("::") @@ -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 = {} @@ -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: diff --git a/patchwork/common/utils/browser_initializer.py b/patchwork/common/utils/browser_initializer.py new file mode 100644 index 000000000..eea72c425 --- /dev/null +++ b/patchwork/common/utils/browser_initializer.py @@ -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 diff --git a/patchwork/logger.py b/patchwork/logger.py index 421de7bd8..2b584e4c0 100644 --- a/patchwork/logger.py +++ b/patchwork/logger.py @@ -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 @@ -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, @@ -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) @@ -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: @@ -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() @@ -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) diff --git a/patchwork/steps/BrowserUse/BrowserUse.py b/patchwork/steps/BrowserUse/BrowserUse.py index 8f8ae2f57..a67a53cd1 100644 --- a/patchwork/steps/BrowserUse/BrowserUse.py +++ b/patchwork/steps/BrowserUse/BrowserUse.py @@ -10,111 +10,6 @@ logger = logging.getLogger(__name__) -# Global variables to cache browser initialization -_browser = None -_controller = None - - -def init_browser(): - """ - Initialize and cache browser and controller instances. - - This function uses a singleton pattern to ensure we only create one browser - instance throughout the application lifecycle, which saves resources. - - Returns: - tuple: (Browser, Controller) instances for web automation - """ - global _browser, _controller - - # Return cached instances if already initialized - if _browser is not None and _controller is not None: - return _browser, _controller - - from browser_use import Browser, BrowserConfig, BrowserContextConfig, Controller - from browser_use.agent.views import ActionResult - from browser_use.browser.context import BrowserContext - - # Set up downloads directory for browser operations - downloads_path = os.path.join(os.getcwd(), "downloads") - if not os.path.exists(downloads_path): - os.makedirs(downloads_path) - - context_config = BrowserContextConfig(save_downloads_path=downloads_path) - config = BrowserConfig(headless=True, disable_security=True, new_context_config=context_config) - controller = Controller() - - # Register custom action to upload files to web elements - @controller.action( - description="Upload file to interactive element with file path", - ) - async def upload_file(index: int, path: str, browser: BrowserContext): - """ - Upload a file to a file input element identified by its index. - - Args: - index: The DOM element index to target - path: Local file path to upload - browser: Browser context for interaction - - Returns: - ActionResult: Result of the upload operation - """ - 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(path) - msg = f"Successfully uploaded file to index {index}" - 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) - - # Register custom action to read file contents - @controller.action(description="Read the file content of a file given a path") - async def read_file(path: str): - """ - Read and return the contents of a file at the specified path. - - Args: - path: Path to the file to read - - Returns: - ActionResult: File contents or error message - """ - if not os.path.exists(path): - return ActionResult(error=f"File {path} does not exist") - - with open(path, "r") as f: - content = f.read() - msg = f"File content: {content}" - logger.info(msg) - return ActionResult(extracted_content=msg, include_in_memory=True) - - # Cache the initialized instances - _browser = Browser(config=config) - _controller = controller - - return _browser, _controller - class BrowserUse(Step, input_class=BrowserUseInputs, output_class=BrowserUseOutputs): """ @@ -156,10 +51,11 @@ def __init__(self, inputs): ) # Configure GIF generation for debugging/visualization - self.generate_gif = ( - f"agent_history_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.gif" - if ("generate_gif" in self.inputs and self.inputs["generate_gif"]) - or ("debug" in self.inputs and self.inputs["debug"]) + self.gif_path = ( + self.inputs.get("gif_path", None) + if "gif_path" in self.inputs + else os.path.join(os.getcwd(), f"agent_history_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.gif") + if ("debug" in self.inputs and self.inputs["debug"]) else False ) @@ -173,21 +69,43 @@ def run(self) -> dict: Returns: dict: Results of the browser automation task """ - from browser_use import Agent + from browser_use import Agent, BrowserConfig + + from patchwork.common.utils.browser_initializer import BrowserInitializer + + browser_config = BrowserConfig( + headless=self.inputs.get("headless", True), + disable_security=True, + ) + browser_context = BrowserInitializer.init_browser_context( + browser_config, self.inputs.get("downloads_path", None) + ) + controller = BrowserInitializer.init_controller() + logger.info("Browser initialized") - browser, controller = init_browser() agent = Agent( - browser=browser, + browser_context=browser_context, controller=controller, task=mustache_render(self.inputs["task"], self.inputs["task_value"]), llm=self.llm, - generate_gif=self.generate_gif, + generate_gif=self.gif_path, validate_output=True, + initial_actions=self.inputs.get("initial_actions", None), + use_vision=self.inputs.get("use_vision", True), ) # Run the agent in an event loop loop = asyncio.new_event_loop() - self.history = loop.run_until_complete(agent.run()) + timeout = self.inputs.get("timeout", 600) # default timeout of 10 minutes + try: + self.history = loop.run_until_complete(asyncio.wait_for(agent.run(), timeout=timeout)) + except asyncio.TimeoutError: + logger.error(f"Agent timed out after {timeout} seconds") + raise + finally: + loop.run_until_complete(browser_context.close()) + loop.run_until_complete(browser_context.browser.close()) + loop.close() # Format results as JSON if schema provided if "example_json" in self.inputs: @@ -196,7 +114,7 @@ def run(self) -> dict: return { "history": self.history, "result": self.history.final_result(), - "generated_gif": self.generate_gif, + "generated_gif": self.gif_path, } def __format_history_as_json(self): diff --git a/patchwork/steps/BrowserUse/typed.py b/patchwork/steps/BrowserUse/typed.py index 114fb0551..69fa47342 100644 --- a/patchwork/steps/BrowserUse/typed.py +++ b/patchwork/steps/BrowserUse/typed.py @@ -1,4 +1,4 @@ -from typing_extensions import Annotated, Any, Dict, Optional, TypedDict +from typing_extensions import Annotated, Any, Dict, Optional, TypedDict, List from patchwork.common.utils.step_typing import StepTypeConfig @@ -13,7 +13,12 @@ class BrowserUseInputs(__BrowserUseInputsRequired, total=False): openai_api_key: Annotated[str, StepTypeConfig(or_op=["google_api_key", "anthropic_api_key"])] anthropic_api_key: Annotated[str, StepTypeConfig(or_op=["google_api_key", "openai_api_key"])] google_api_key: Annotated[str, StepTypeConfig(or_op=["openai_api_key", "anthropic_api_key"])] - generate_gif: Optional[bool] + gif_path: Optional[str] + headless: Optional[bool] + initial_actions: Optional[List[Dict[str, Dict[str, Any]]]] + downloads_path: Optional[str] + use_vision: Optional[bool] + timeout: Optional[int] # optional timeout in seconds, defaults to 600 if not provided class BrowserUseOutputs(TypedDict): diff --git a/poetry.lock b/poetry.lock index 557915e18..647f9e91e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1318,6 +1318,20 @@ files = [ {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, ] +[[package]] +name = "dotenv" +version = "0.9.9" +description = "Deprecated package" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"}, +] + +[package.dependencies] +python-dotenv = "*" + [[package]] name = "dulwich" version = "0.21.7" @@ -5167,10 +5181,9 @@ six = ">=1.5" name = "python-dotenv" version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.11\" and (extra == \"browser-use\" or extra == \"all\")" files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -5846,6 +5859,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -5854,6 +5868,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -5862,6 +5877,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -5870,6 +5886,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -5878,6 +5895,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -7493,4 +7511,4 @@ security = ["owasp-depscan", "semgrep"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "89b2eff98a6406c0fe0038c498682023108e047cb51050df19145c873d08b9fa" +content-hash = "2dc7485a8c4b2fa12b5d77f8ab7d8d6e0af56e1939344fb4522a1829d7617d8f" diff --git a/pyproject.toml b/pyproject.toml index df1fd225e..3e9dad45e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ +```toml [tool.poetry] name = "patchwork-cli" version = "0.0.119" @@ -54,6 +55,7 @@ csvkit = "^2.1.0" python-magic = "^0.4.27" scikit-learn = "^1.3.2" json-repair = "~0.30.0" +dotenv = "^0.9.9" # pinning transitive dependencies tree-sitter = "~0.21.3" numpy = "1.26.4" @@ -92,7 +94,7 @@ setuptools = "*" poethepoet = { version = "^0.27.0", extras = ["poetry-plugin"] } mypy = "^1.7.1" types-requests = "~2.31.0" -black = "^23.12.0" +black = "^24.3.0" isort = "^5.13.2" autoflake = "^2.3.1" pytest = "^8.1.1" @@ -135,3 +137,4 @@ in-place = true remove-all-unused-imports = true expand-star-imports = true ignore-init-module-imports = true +```