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/patchwork/steps/FileAgent/README.md b/patchwork/steps/FileAgent/README.md new file mode 100644 index 000000000..e321851e4 --- /dev/null +++ b/patchwork/steps/FileAgent/README.md @@ -0,0 +1,52 @@ +# Overview + +The code provided consists of three files that collectively define a `FileAgent`, which is a component for handling file operations with language model capabilities. It is a part of a larger framework called Patchwork. + +## File: `typed.py` + +### Description + +This module defines the input and output types for the `FileAgent`. It uses Python type annotations to ensure the correct structure and types for interactions with the `FileAgent`. + +### Inputs + +- `task`: A string defining the task that needs to be performed. +- `base_path`: (Optional) A string specifying the base file path for operations. +- `prompt_value`: A dictionary for dynamic data to use in prompts. +- `max_llm_calls`: An integer, annotated as a configuration setting, defining the maximum allowable LLM calls. +- `anthropic_api_key`: A string, annotated as a configuration setting, used for API authentication. + +### Outputs + +- `request_tokens`: An integer indicating the number of tokens requested. +- `response_tokens`: An integer indicating the number of tokens received in response. + +## File: `FileAgent.py` + +### Description + +This file contains the main logic for the `FileAgent`. It extends the behavior of a `Step` from the Patchwork framework, orchestrating tasks with language models for file-related operations, such as converting and analyzing tabular data formats. + +### Key Components + +- **Initialization**: Sets up parameters including file paths, API clients, and task-specific prompts. +- **Agent Configuration**: Defines a language agent using `AgentConfig`, specifying a model and a list of tools usable by the agent. +- **Run Method**: Executes the file agent process, utilizing language model strategies and tools for file manipulation tasks. + +### Tools and Strategies + +- **FileViewTool**: For displaying file content. +- **FindTextTool**: To search text within files. +- **In2CSVTool**: For converting files to CSV format. +- **CSVSQLTool**: For handling SQL-like operations on CSV data. +- **AgenticStrategyV2**: Executes the defined strategy using the model and tools. + +## File: `__init__.py` + +### Description + +This is an empty Python initialization file, indicating that the directory can be treated as a Python package. It suggests modular organization but does not contain additional logic or data. + +### Usage + +This setup allows developers and engineers to integrate file handling capabilities with automated language model interactions in applications that involve file manipulations, particularly with tabular data. By utilizing predefined input and output types, as well as the strategic application of automated agents, tasks such as data extraction and conversion can be efficiently automated within larger workflows. 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..067c57174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,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"