From 91e35f3b3097d23d09f4605dbcd59f5d29b42063 Mon Sep 17 00:00:00 2001 From: harshit0605 Date: Mon, 6 Apr 2026 22:42:19 +0530 Subject: [PATCH] feat: add Camoufox browser runtime support --- crawl4ai/async_configs.py | 202 ++++++- crawl4ai/async_crawler_strategy.py | 33 +- crawl4ai/browser_manager.py | 649 ++++++++++++++-------- docs/examples/camoufox_example.py | 49 ++ docs/examples/camoufox_proxy_example.py | 56 ++ docs/md_v2/advanced/camoufox-browser.md | 200 +++++++ docs/md_v2/core/browser-crawler-config.md | 61 +- docs/md_v2/core/examples.md | 2 + mkdocs.yml | 3 +- pyproject.toml | 4 +- tests/regression/test_reg_camoufox.py | 126 +++++ tests/unit/test_camoufox_runtime.py | 321 +++++++++++ uv.lock | 497 +++++++++++++++-- 13 files changed, 1879 insertions(+), 324 deletions(-) create mode 100644 docs/examples/camoufox_example.py create mode 100644 docs/examples/camoufox_proxy_example.py create mode 100644 docs/md_v2/advanced/camoufox-browser.md create mode 100644 tests/regression/test_reg_camoufox.py create mode 100644 tests/unit/test_camoufox_runtime.py diff --git a/crawl4ai/async_configs.py b/crawl4ai/async_configs.py index 37b5480d9..8de0e7d8c 100644 --- a/crawl4ai/async_configs.py +++ b/crawl4ai/async_configs.py @@ -37,6 +37,57 @@ UrlMatcher = Union[str, Callable[[str], bool], List[Union[str, Callable[[str], bool]]]] +LEGACY_DEFAULT_CHROMIUM_USER_AGENT = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + "Chrome/116.0.0.0 Safari/537.36" +) +DEFAULT_FIREFOX_USER_AGENT = ( + "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) " + "Gecko/20100101 Firefox/135.0" +) +CAMOUFOX_RUNTIME = "camoufox" +CAMOUFOX_IDENTITY_HEADER_NAMES = {"user-agent", "accept-language"} +CAMOUFOX_IDENTITY_HEADER_PREFIXES = ("sec-ch-ua",) +CAMOUFOX_BLOCKED_RUN_FIELDS = ( + "proxy_config", + "user_agent", + "user_agent_mode", + "locale", + "timezone_id", + "geolocation", + "override_navigator", + "magic", +) + + +def _header_names(headers: Optional[Dict[str, Any]]) -> List[str]: + if not headers: + return [] + return [str(name).lower() for name in headers.keys()] + + +def _find_identity_headers(headers: Optional[Dict[str, Any]]) -> List[str]: + matches = [] + for name in _header_names(headers): + if name in CAMOUFOX_IDENTITY_HEADER_NAMES or name.startswith( + CAMOUFOX_IDENTITY_HEADER_PREFIXES + ): + matches.append(name) + return matches + + +def _config_value_is_set(value: Any) -> bool: + if value is None: + return False + if isinstance(value, bool): + return value + if isinstance(value, str): + return value != "" + if isinstance(value, (list, tuple, set, dict)): + return len(value) > 0 + return True + + def _with_defaults(cls): """Class decorator: adds set_defaults/get_defaults/reset_defaults classmethods. @@ -498,6 +549,8 @@ class BrowserConfig: code will then reference these settings to initialize the browser in a consistent, documented manner. Attributes: + browser_runtime (str): Browser automation runtime. Supported values: "playwright" and "camoufox". + Default: "playwright". browser_type (str): The type of browser to launch. Supported values: "chromium", "firefox", "webkit". Default: "chromium". headless (bool): Whether to run the browser in headless mode (no visible GUI). @@ -508,6 +561,7 @@ class BrowserConfig: "cdp" - use explicit CDP settings provided in cdp_url "docker" - run browser in Docker container with isolation Default: "dedicated" + Note: Camoufox currently supports only "dedicated". use_managed_browser (bool): Launch the browser using a managed approach (e.g., via CDP), allowing advanced manipulation. Default: False. cdp_url (str): URL for the Chrome DevTools Protocol (CDP) endpoint. Default: "ws://localhost:9222/devtools/browser/". @@ -573,12 +627,14 @@ class BrowserConfig: Default: []. headers (dict): Extra HTTP headers to apply to all requests in this context. Default: {}. - user_agent (str): Custom User-Agent string to use. Default: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36". + user_agent (str or None): Custom User-Agent string to use. If None, Crawl4AI derives a browser-specific + default. Camoufox rejects explicit overrides. user_agent_mode (str or None): Mode for generating the user agent (e.g., "random"). If None, use the provided user_agent as-is. Default: None. user_agent_generator_config (dict or None): Configuration for user agent generation if user_agent_mode is set. Default: None. + camoufox_options (dict or None): Pass-through options for Camoufox launch and fingerprint configuration. + Default: None. text_mode (bool): If True, disables images and other rich content for potentially faster load times. Default: False. light_mode (bool): Disables certain background features for performance gains. Default: False. @@ -603,6 +659,7 @@ class BrowserConfig: def __init__( self, + browser_runtime: str = "playwright", browser_type: str = "chromium", headless: bool = True, browser_mode: str = "dedicated", @@ -633,14 +690,10 @@ def __init__( verbose: bool = True, cookies: list = None, headers: dict = None, - user_agent: str = ( - # "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) AppleWebKit/537.36 " - # "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " - # "(KHTML, like Gecko) Chrome/116.0.5845.187 Safari/604.1 Edg/117.0.2045.47" - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/116.0.0.0 Safari/537.36" - ), + user_agent: Optional[str] = None, user_agent_mode: str = "", - user_agent_generator_config: dict = {}, + user_agent_generator_config: dict = None, + camoufox_options: Optional[Dict[str, Any]] = None, text_mode: bool = False, light_mode: bool = False, extra_args: list = None, @@ -653,7 +706,13 @@ def __init__( memory_saving_mode: bool = False, max_pages_before_recycle: int = 0, ): - + requested_user_agent = user_agent + requested_headers = copy.deepcopy(headers) if headers is not None else {} + requested_camoufox_options = ( + copy.deepcopy(camoufox_options) if camoufox_options is not None else None + ) + + self.browser_runtime = browser_runtime self.browser_type = browser_type self.headless = headless self.browser_mode = browser_mode @@ -703,9 +762,10 @@ def __init__( self.java_script_enabled = java_script_enabled self.cookies = cookies if cookies is not None else [] self.headers = headers if headers is not None else {} - self.user_agent = user_agent + self.user_agent = user_agent or self._default_user_agent() self.user_agent_mode = user_agent_mode - self.user_agent_generator_config = user_agent_generator_config + self.user_agent_generator_config = user_agent_generator_config or {} + self.camoufox_options = camoufox_options if camoufox_options is not None else None self.text_mode = text_mode self.light_mode = light_mode self.extra_args = extra_args if extra_args is not None else [] @@ -720,16 +780,25 @@ def __init__( self.memory_saving_mode = memory_saving_mode self.max_pages_before_recycle = max_pages_before_recycle + self._validate_runtime_inputs( + requested_user_agent=requested_user_agent, + requested_headers=requested_headers, + requested_camoufox_options=requested_camoufox_options, + ) + fa_user_agenr_generator = ValidUAGenerator() if self.user_agent_mode == "random": self.user_agent = fa_user_agenr_generator.generate( **(self.user_agent_generator_config or {}) ) - else: - pass - self.browser_hint = UAGen.generate_client_hints(self.user_agent) - self.headers.setdefault("sec-ch-ua", self.browser_hint) + self.browser_hint = ( + UAGen.generate_client_hints(self.user_agent) + if self.browser_type == "chromium" + else "" + ) + if self.browser_hint and not self.uses_browser_scoped_identity: + self.headers.setdefault("sec-ch-ua", self.browser_hint) # Set appropriate browser management flags based on browser_mode if self.browser_mode == "builtin": @@ -758,6 +827,105 @@ def __init__( "Stealth mode requires a dedicated browser instance." ) + @property + def is_camoufox(self) -> bool: + return self.browser_runtime == CAMOUFOX_RUNTIME + + @property + def uses_browser_scoped_identity(self) -> bool: + return self.is_camoufox + + def _default_user_agent(self) -> str: + if self.browser_type == "firefox": + return DEFAULT_FIREFOX_USER_AGENT + return LEGACY_DEFAULT_CHROMIUM_USER_AGENT + + def _validate_runtime_inputs( + self, + requested_user_agent: Optional[str], + requested_headers: Dict[str, Any], + requested_camoufox_options: Optional[Dict[str, Any]], + ) -> None: + if self.browser_runtime not in {"playwright", CAMOUFOX_RUNTIME}: + raise ValueError( + "browser_runtime must be one of: 'playwright', 'camoufox'." + ) + + if not self.is_camoufox: + return + + if self.browser_type != "firefox": + raise ValueError( + "browser_runtime='camoufox' requires browser_type='firefox'." + ) + if self.browser_mode != "dedicated": + raise ValueError( + "browser_runtime='camoufox' currently supports only " + "browser_mode='dedicated'." + ) + if self.cdp_url: + raise ValueError( + "browser_runtime='camoufox' does not support cdp_url or CDP-based " + "browser connections." + ) + if self.use_managed_browser and not self.use_persistent_context: + raise ValueError( + "browser_runtime='camoufox' does not support managed-browser " + "startup outside use_persistent_context=True." + ) + if self.use_persistent_context and self.storage_state: + raise ValueError( + "browser_runtime='camoufox' does not support storage_state when " + "use_persistent_context=True. Reuse browser state through " + "user_data_dir instead." + ) + if self.enable_stealth: + raise ValueError( + "browser_runtime='camoufox' cannot be combined with enable_stealth. " + "Camoufox must own stealth and fingerprint behavior." + ) + if ( + _config_value_is_set(requested_user_agent) + and requested_user_agent != self._default_user_agent() + ): + raise ValueError( + "browser_runtime='camoufox' does not accept BrowserConfig.user_agent. " + "Use camoufox_options for browser-scoped identity settings." + ) + if _config_value_is_set(self.user_agent_mode): + raise ValueError( + "browser_runtime='camoufox' does not accept BrowserConfig.user_agent_mode. " + "Use camoufox_options for browser-scoped identity settings." + ) + bad_headers = _find_identity_headers(requested_headers) + if bad_headers: + rendered = ", ".join(sorted(set(bad_headers))) + raise ValueError( + "browser_runtime='camoufox' does not allow identity-bearing " + f"BrowserConfig.headers: {rendered}. Use camoufox_options instead." + ) + if self.proxy_config and requested_camoufox_options and "proxy" in requested_camoufox_options: + raise ValueError( + "browser_runtime='camoufox' received both proxy_config and " + "camoufox_options['proxy']. Set only one proxy source." + ) + + def validate_crawler_run_config(self, crawler_run_config: Optional["CrawlerRunConfig"]) -> None: + if not self.is_camoufox or crawler_run_config is None: + return + + invalid_fields = [] + for field_name in CAMOUFOX_BLOCKED_RUN_FIELDS: + if _config_value_is_set(getattr(crawler_run_config, field_name, None)): + invalid_fields.append(field_name) + + if invalid_fields: + rendered = ", ".join(invalid_fields) + raise ValueError( + "browser_runtime='camoufox' requires browser-scoped identity. " + f"Remove these CrawlerRunConfig overrides: {rendered}." + ) + @staticmethod def from_kwargs(kwargs: dict) -> "BrowserConfig": # Auto-deserialize any dict values that use the {"type": ..., "params": ...} @@ -773,6 +941,7 @@ def from_kwargs(kwargs: dict) -> "BrowserConfig": def to_dict(self): result = { + "browser_runtime": self.browser_runtime, "browser_type": self.browser_type, "headless": self.headless, "browser_mode": self.browser_mode, @@ -801,6 +970,7 @@ def to_dict(self): "user_agent": self.user_agent, "user_agent_mode": self.user_agent_mode, "user_agent_generator_config": self.user_agent_generator_config, + "camoufox_options": self.camoufox_options, "text_mode": self.text_mode, "light_mode": self.light_mode, "extra_args": self.extra_args, diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index b9de25f6b..28d088192 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -73,7 +73,12 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): """ def __init__( - self, browser_config: BrowserConfig = None, logger: AsyncLogger = None, browser_adapter: BrowserAdapter = None, **kwargs + self, + browser_config: BrowserConfig = None, + logger: AsyncLogger = None, + browser_adapter: BrowserAdapter = None, + browser_manager_factory: Callable[..., BrowserManager] = None, + **kwargs, ): """ Initialize the AsyncPlaywrightCrawlerStrategy with a browser configuration. @@ -93,6 +98,11 @@ def __init__( # Initialize browser adapter self.adapter = browser_adapter or PlaywrightAdapter() + if self.browser_config.is_camoufox and isinstance(self.adapter, UndetectedAdapter): + raise ValueError( + "browser_runtime='camoufox' cannot be combined with UndetectedAdapter. " + "Camoufox must own stealth and fingerprint behavior." + ) # Initialize session management self._downloaded_files = [] @@ -111,7 +121,8 @@ def __init__( } # Initialize browser manager with config - self.browser_manager = BrowserManager( + browser_manager_cls = browser_manager_factory or BrowserManager + self.browser_manager = browser_manager_cls( browser_config=self.browser_config, logger=self.logger, use_undetected=isinstance(self.adapter, UndetectedAdapter) @@ -545,7 +556,10 @@ async def _crawl_web( # changing it here would only desync browser_config from reality. # Users should set user_agent or user_agent_mode on BrowserConfig. ua_changed = False - if not self.browser_config.use_persistent_context: + if ( + not self.browser_config.use_persistent_context + and not self.browser_config.uses_browser_scoped_identity + ): user_agent_to_override = config.user_agent if user_agent_to_override: self.browser_config.user_agent = user_agent_to_override @@ -558,10 +572,15 @@ async def _crawl_web( # Keep sec-ch-ua in sync whenever the UA changed if ua_changed: - self.browser_config.browser_hint = UAGen.generate_client_hints( - self.browser_config.user_agent + self.browser_config.browser_hint = ( + UAGen.generate_client_hints(self.browser_config.user_agent) + if self.browser_config.browser_type == "chromium" + else "" ) - self.browser_config.headers["sec-ch-ua"] = self.browser_config.browser_hint + if self.browser_config.browser_hint: + self.browser_config.headers["sec-ch-ua"] = self.browser_config.browser_hint + else: + self.browser_config.headers.pop("sec-ch-ua", None) # Get page for session page, context = await self.browser_manager.get_page(crawlerRunConfig=config) @@ -595,7 +614,7 @@ async def _crawl_web( # at context level by setup_context(). This fallback covers # managed-browser / persistent / CDP paths where setup_context() # is called without a crawlerRunConfig. - if config.override_navigator or config.simulate_user or config.magic: + if self.browser_manager._should_inject_navigator_overrides(config): if not getattr(context, '_crawl4ai_nav_overrider_injected', False): await context.add_init_script(load_js_script("navigator_overrider")) context._crawl4ai_nav_overrider_injected = True diff --git a/crawl4ai/browser_manager.py b/crawl4ai/browser_manager.py index 0b429c34d..99a529ffe 100644 --- a/crawl4ai/browser_manager.py +++ b/crawl4ai/browser_manager.py @@ -1,4 +1,6 @@ import asyncio +import copy +import importlib import time from typing import Dict, List, Optional, Tuple import os @@ -667,6 +669,253 @@ async def close_all(cls): cls._cache.clear() +class _PlaywrightRuntimeBackend: + runtime_name = "playwright" + requires_playwright = True + supports_managed_browser = True + applies_browser_scoped_identity = False + + def __init__(self, manager: "BrowserManager"): + self.manager = manager + + def _proxy_settings(self): + if not self.manager.config.proxy_config: + return None + return { + "server": self.manager.config.proxy_config.server, + "username": self.manager.config.proxy_config.username, + "password": self.manager.config.proxy_config.password, + } + + def build_browser_args(self) -> dict: + args = [] + if self.manager.config.browser_type == "chromium": + args = [ + "--disable-gpu", + "--disable-gpu-compositing", + "--disable-software-rasterizer", + "--no-sandbox", + "--disable-dev-shm-usage", + "--no-first-run", + "--no-default-browser-check", + "--disable-infobars", + "--window-position=0,0", + "--ignore-certificate-errors", + "--ignore-certificate-errors-spki-list", + "--disable-blink-features=AutomationControlled", + "--window-position=400,0", + "--disable-renderer-backgrounding", + "--disable-ipc-flooding-protection", + "--force-color-profile=srgb", + "--mute-audio", + "--disable-background-timer-throttling", + "--disable-features=OptimizationHints,MediaRouter,DialMediaRouteProvider", + "--disable-component-update", + "--disable-domain-reliability", + f"--window-size={self.manager.config.viewport_width},{self.manager.config.viewport_height}", + ] + + if self.manager.config.memory_saving_mode: + args.extend( + [ + "--aggressive-cache-discard", + "--js-flags=--max-old-space-size=512", + ] + ) + + if self.manager.config.light_mode: + args.extend(BROWSER_DISABLE_OPTIONS) + + if self.manager.config.text_mode: + args.extend( + [ + "--blink-settings=imagesEnabled=false", + "--disable-remote-fonts", + "--disable-images", + "--disable-javascript", + "--disable-software-rasterizer", + "--disable-dev-shm-usage", + ] + ) + + if self.manager.config.extra_args: + args.extend(self.manager.config.extra_args) + + args = list(dict.fromkeys(args)) + browser_args = {"headless": self.manager.config.headless} + if args: + browser_args["args"] = args + + if self.manager.config.browser_type == "chromium" and self.manager.config.chrome_channel: + browser_args["channel"] = self.manager.config.chrome_channel + + if self.manager.config.accept_downloads: + browser_args["downloads_path"] = self.manager.config.downloads_path or os.path.join( + os.getcwd(), "downloads" + ) + os.makedirs(browser_args["downloads_path"], exist_ok=True) + + if self.manager.config.proxy: + warnings.warn( + "BrowserConfig.proxy is deprecated and ignored. Use proxy_config instead.", + DeprecationWarning, + ) + proxy_settings = self._proxy_settings() + if proxy_settings: + browser_args["proxy"] = proxy_settings + + return browser_args + + def build_persistent_launch_kwargs(self) -> dict: + cli_args = [] + if self.manager.config.browser_type == "chromium": + skip_prefixes = ( + "--proxy-server", + "--remote-debugging-port", + "--user-data-dir", + "--headless", + "--window-size", + ) + cli_args = [ + flag + for flag in ManagedBrowser.build_browser_flags(self.manager.config) + if not flag.startswith(skip_prefixes) + ] + + if self.manager.config.extra_args: + cli_args.extend(self.manager.config.extra_args) + + launch_kwargs = { + "headless": self.manager.config.headless, + "viewport": { + "width": self.manager.config.viewport_width, + "height": self.manager.config.viewport_height, + }, + "user_agent": self.manager.config.user_agent or None, + "ignore_https_errors": self.manager.config.ignore_https_errors, + "accept_downloads": self.manager.config.accept_downloads, + } + + cli_args = list(dict.fromkeys(cli_args)) + if cli_args: + launch_kwargs["args"] = cli_args + + if self.manager.config.browser_type == "chromium" and self.manager.config.chrome_channel: + launch_kwargs["channel"] = self.manager.config.chrome_channel + + proxy_settings = self._proxy_settings() + if proxy_settings: + launch_kwargs["proxy"] = proxy_settings + + if self.manager.config.storage_state: + launch_kwargs["storage_state"] = self.manager.config.storage_state + + return launch_kwargs + + async def launch_persistent_context(self, user_data_dir: str): + browser_type = getattr(self.manager.playwright, self.manager.config.browser_type) + return await browser_type.launch_persistent_context( + user_data_dir, + **self.build_persistent_launch_kwargs(), + ) + + async def launch_browser(self, browser_args: dict): + browser_type = getattr(self.manager.playwright, self.manager.config.browser_type) + return await browser_type.launch(**browser_args) + + async def connect_managed_browser(self, cdp_url: str): + return await self.manager.playwright.chromium.connect_over_cdp(cdp_url) + + async def close(self): + return None + + +class _CamoufoxRuntimeBackend: + runtime_name = "camoufox" + requires_playwright = False + supports_managed_browser = False + applies_browser_scoped_identity = True + + def __init__(self, manager: "BrowserManager"): + self.manager = manager + self._context_manager = None + + def _import_async_camoufox(self): + try: + module = importlib.import_module("camoufox.async_api") + return module.AsyncCamoufox + except ImportError as exc: + raise RuntimeError( + "browser_runtime='camoufox' requires the optional Camoufox dependency. " + "Install it with `pip install 'crawl4ai[camoufox]'` or " + "`pip install -U camoufox[geoip]`, then run `python -m camoufox fetch`." + ) from exc + + def _proxy_settings(self): + if not self.manager.config.proxy_config: + return None + return { + "server": self.manager.config.proxy_config.server, + "username": self.manager.config.proxy_config.username, + "password": self.manager.config.proxy_config.password, + } + + def build_browser_args(self) -> dict: + launch_kwargs = copy.deepcopy(self.manager.config.camoufox_options or {}) + launch_kwargs.setdefault("headless", self.manager.config.headless) + + proxy_settings = self._proxy_settings() + if proxy_settings: + launch_kwargs["proxy"] = proxy_settings + + return launch_kwargs + + def build_persistent_launch_kwargs(self, user_data_dir: str) -> dict: + launch_kwargs = self.build_browser_args() + launch_kwargs.update( + { + "persistent_context": True, + "user_data_dir": user_data_dir, + "viewport": { + "width": self.manager.config.viewport_width, + "height": self.manager.config.viewport_height, + }, + "ignore_https_errors": self.manager.config.ignore_https_errors, + "accept_downloads": self.manager.config.accept_downloads, + "java_script_enabled": self.manager.config.java_script_enabled, + "device_scale_factor": self.manager.config.device_scale_factor, + } + ) + if self.manager.config.storage_state: + launch_kwargs["storage_state"] = self.manager.config.storage_state + if self.manager.config.downloads_path: + launch_kwargs["downloads_path"] = self.manager.config.downloads_path + return launch_kwargs + + async def launch_persistent_context(self, user_data_dir: str): + AsyncCamoufox = self._import_async_camoufox() + self._context_manager = AsyncCamoufox( + **self.build_persistent_launch_kwargs(user_data_dir) + ) + return await self._context_manager.__aenter__() + + async def launch_browser(self, browser_args: dict): + AsyncCamoufox = self._import_async_camoufox() + self._context_manager = AsyncCamoufox(**browser_args) + return await self._context_manager.__aenter__() + + async def connect_managed_browser(self, cdp_url: str): + raise RuntimeError( + "browser_runtime='camoufox' does not support managed CDP browser connections." + ) + + async def close(self): + if self._context_manager is None: + return + await self._context_manager.__aexit__(None, None, None) + self._context_manager = None + + class BrowserManager: """ Manages the browser instance and context. @@ -718,6 +967,11 @@ def __init__(self, browser_config: BrowserConfig, logger=None, use_undetected: b self.config: BrowserConfig = browser_config self.logger = logger self.use_undetected = use_undetected + self.runtime_backend = ( + _CamoufoxRuntimeBackend(self) + if self.config.is_camoufox + else _PlaywrightRuntimeBackend(self) + ) # Browser state self.browser = None @@ -765,7 +1019,7 @@ def __init__(self, browser_config: BrowserConfig, logger=None, use_undetected: b self._stealth_adapter = StealthAdapter() # Initialize ManagedBrowser if needed - if self.config.use_managed_browser: + if self.config.use_managed_browser and self.runtime_backend.supports_managed_browser: self.managed_browser = ManagedBrowser( browser_type=self.config.browser_type, user_data_dir=self.config.user_data_dir, @@ -788,84 +1042,41 @@ async def start(self): Note: This method should be called in a separate task to avoid blocking the main event loop. """ - if self.playwright is not None: + if self.playwright is not None or self.browser is not None or self.default_context is not None: await self.close() - # Use cached CDP connection if enabled and cdp_url is set - if self.config.cache_cdp_connection and self.config.cdp_url: - self._using_cached_cdp = True - self.config.use_managed_browser = True - self.playwright, self.browser = await _CDPConnectionCache.acquire( - self.config.cdp_url, self.use_undetected - ) - else: - self._using_cached_cdp = False - if self.use_undetected: - from patchright.async_api import async_playwright + if self.runtime_backend.requires_playwright: + # Use cached CDP connection if enabled and cdp_url is set + if self.config.cache_cdp_connection and self.config.cdp_url: + self._using_cached_cdp = True + self.config.use_managed_browser = True + self.playwright, self.browser = await _CDPConnectionCache.acquire( + self.config.cdp_url, self.use_undetected + ) else: - from playwright.async_api import async_playwright + self._using_cached_cdp = False + if self.use_undetected: + from patchright.async_api import async_playwright + else: + from playwright.async_api import async_playwright - # Initialize playwright - self.playwright = await async_playwright().start() + self.playwright = await async_playwright().start() + else: + self._using_cached_cdp = False + self.playwright = None - # ── Persistent context via Playwright's native API ────────────── # When use_persistent_context is set and we're not connecting to an - # external CDP endpoint, use launch_persistent_context() instead of - # subprocess + CDP. This properly supports proxy authentication - # (server + username + password) which the --proxy-server CLI flag - # cannot handle. + # external CDP endpoint, use the runtime's persistent context path. if ( self.config.use_persistent_context and not self.config.cdp_url and not self._using_cached_cdp ): - # Collect stealth / optimization CLI flags, excluding ones that - # launch_persistent_context handles via keyword arguments. - _skip_prefixes = ( - "--proxy-server", - "--remote-debugging-port", - "--user-data-dir", - "--headless", - "--window-size", - ) - cli_args = [ - flag - for flag in ManagedBrowser.build_browser_flags(self.config) - if not flag.startswith(_skip_prefixes) - ] - if self.config.extra_args: - cli_args.extend(self.config.extra_args) - - launch_kwargs = { - "headless": self.config.headless, - "args": list(dict.fromkeys(cli_args)), # dedupe - "viewport": { - "width": self.config.viewport_width, - "height": self.config.viewport_height, - }, - "user_agent": self.config.user_agent or None, - "ignore_https_errors": self.config.ignore_https_errors, - "accept_downloads": self.config.accept_downloads, - } - - if self.config.proxy_config: - launch_kwargs["proxy"] = { - "server": self.config.proxy_config.server, - "username": self.config.proxy_config.username, - "password": self.config.proxy_config.password, - } - - if self.config.storage_state: - launch_kwargs["storage_state"] = self.config.storage_state - user_data_dir = self.config.user_data_dir or tempfile.mkdtemp( prefix="crawl4ai-persistent-" ) - - self.default_context = ( - await self.playwright.chromium.launch_persistent_context( - user_data_dir, **launch_kwargs - ) + self.default_context = await self.runtime_backend.launch_persistent_context( + user_data_dir ) self.browser = None # persistent context has no separate Browser self._launched_persistent = True @@ -878,7 +1089,9 @@ async def start(self): BrowserManager._global_pages_in_use[self._browser_endpoint_key] = set() return - if self.config.cdp_url or self.config.use_managed_browser: + if self.config.cdp_url or ( + self.config.use_managed_browser and self.runtime_backend.supports_managed_browser + ): self.config.use_managed_browser = True if not self._using_cached_cdp: @@ -888,7 +1101,7 @@ async def start(self): if not await self._verify_cdp_ready(cdp_url): raise Exception(f"CDP endpoint at {cdp_url} is not ready after startup") - self.browser = await self.playwright.chromium.connect_over_cdp(cdp_url) + self.browser = await self.runtime_backend.connect_managed_browser(cdp_url) contexts = self.browser.contexts @@ -928,15 +1141,7 @@ async def start(self): await self.setup_context(self.default_context) else: browser_args = self._build_browser_args() - - # Launch appropriate browser type - if self.config.browser_type == "firefox": - self.browser = await self.playwright.firefox.launch(**browser_args) - elif self.config.browser_type == "webkit": - self.browser = await self.playwright.webkit.launch(**browser_args) - else: - self.browser = await self.playwright.chromium.launch(**browser_args) - + self.browser = await self.runtime_backend.launch_browser(browser_args) self.default_context = self.browser # Set the browser endpoint key for global page tracking @@ -1055,88 +1260,95 @@ async def _verify_cdp_ready(self, cdp_url: str) -> bool: return False def _build_browser_args(self) -> dict: - """Build browser launch arguments from config.""" - args = [ - "--disable-gpu", - "--disable-gpu-compositing", - "--disable-software-rasterizer", - "--no-sandbox", - "--disable-dev-shm-usage", - "--no-first-run", - "--no-default-browser-check", - "--disable-infobars", - "--window-position=0,0", - "--ignore-certificate-errors", - "--ignore-certificate-errors-spki-list", - "--disable-blink-features=AutomationControlled", - "--window-position=400,0", - "--disable-renderer-backgrounding", - "--disable-ipc-flooding-protection", - "--force-color-profile=srgb", - "--mute-audio", - "--disable-background-timer-throttling", - # Memory-saving flags: disable unused Chrome features - "--disable-features=OptimizationHints,MediaRouter,DialMediaRouteProvider", - "--disable-component-update", - "--disable-domain-reliability", - # "--single-process", - f"--window-size={self.config.viewport_width},{self.config.viewport_height}", - ] + """Build runtime-specific browser launch arguments from config.""" + return self.runtime_backend.build_browser_args() - if self.config.memory_saving_mode: - args.extend([ - "--aggressive-cache-discard", - '--js-flags=--max-old-space-size=512', - ]) + def _combined_browser_headers(self) -> Dict[str, str]: + combined_headers = dict(self.config.headers or {}) + if self.config.uses_browser_scoped_identity: + return combined_headers - if self.config.light_mode: - args.extend(BROWSER_DISABLE_OPTIONS) + if self.config.user_agent: + combined_headers = {"User-Agent": self.config.user_agent, **combined_headers} + if self.config.browser_hint: + combined_headers = {"sec-ch-ua": self.config.browser_hint, **combined_headers} + return combined_headers + + def _should_inject_navigator_overrides( + self, crawlerRunConfig: CrawlerRunConfig | None + ) -> bool: + if crawlerRunConfig is None: + return False + if crawlerRunConfig.override_navigator or crawlerRunConfig.magic: + return True + return ( + crawlerRunConfig.simulate_user + and not self.config.uses_browser_scoped_identity + ) - if self.config.text_mode: - args.extend( - [ - "--blink-settings=imagesEnabled=false", - "--disable-remote-fonts", - "--disable-images", - "--disable-javascript", - "--disable-software-rasterizer", - "--disable-dev-shm-usage", - ] - ) + def _proxy_settings_for_context(self, proxy_config): + if proxy_config is None: + return None + return { + "server": proxy_config.server, + "username": proxy_config.username, + "password": proxy_config.password, + } - if self.config.extra_args: - args.extend(self.config.extra_args) + def _build_context_settings( + self, crawlerRunConfig: CrawlerRunConfig | None = None + ) -> Dict[str, object]: + self.config.validate_crawler_run_config(crawlerRunConfig) - # Deduplicate args - args = list(dict.fromkeys(args)) - - browser_args = {"headless": self.config.headless, "args": args} + context_settings: Dict[str, object] = { + "viewport": { + "width": self.config.viewport_width, + "height": self.config.viewport_height, + }, + "accept_downloads": self.config.accept_downloads, + "storage_state": self.config.storage_state, + "ignore_https_errors": self.config.ignore_https_errors, + "device_scale_factor": self.config.device_scale_factor, + "java_script_enabled": self.config.java_script_enabled, + } - if self.config.chrome_channel: - browser_args["channel"] = self.config.chrome_channel + if not self.config.uses_browser_scoped_identity: + user_agent = self.config.headers.get("User-Agent", self.config.user_agent) + if user_agent: + context_settings["user_agent"] = user_agent - if self.config.accept_downloads: - browser_args["downloads_path"] = self.config.downloads_path or os.path.join( - os.getcwd(), "downloads" - ) - os.makedirs(browser_args["downloads_path"], exist_ok=True) + proxy_settings = self._proxy_settings_for_context(self.config.proxy_config) + if crawlerRunConfig and crawlerRunConfig.proxy_config: + proxy_settings = self._proxy_settings_for_context( + crawlerRunConfig.proxy_config + ) + if proxy_settings: + context_settings["proxy"] = proxy_settings - if self.config.proxy: - warnings.warn( - "BrowserConfig.proxy is deprecated and ignored. Use proxy_config instead.", - DeprecationWarning, - ) - if self.config.proxy_config: - from playwright.async_api import ProxySettings + if crawlerRunConfig: + if crawlerRunConfig.locale: + context_settings["locale"] = crawlerRunConfig.locale + if crawlerRunConfig.timezone_id: + context_settings["timezone_id"] = crawlerRunConfig.timezone_id + if crawlerRunConfig.geolocation: + context_settings["geolocation"] = { + "latitude": crawlerRunConfig.geolocation.latitude, + "longitude": crawlerRunConfig.geolocation.longitude, + "accuracy": crawlerRunConfig.geolocation.accuracy, + } + permissions = list(context_settings.get("permissions", [])) + permissions.append("geolocation") + context_settings["permissions"] = permissions - proxy_settings = ProxySettings( - server=self.config.proxy_config.server, - username=self.config.proxy_config.username, - password=self.config.proxy_config.password, + if self.config.text_mode: + context_settings.update( + { + "has_touch": False, + "is_mobile": False, + } ) - browser_args["proxy"] = proxy_settings - return browser_args + return context_settings async def setup_context( self, @@ -1172,6 +1384,8 @@ async def setup_context( Returns: None """ + self.config.validate_crawler_run_config(crawlerRunConfig) + if self.config.headers: await context.set_extra_http_headers(self.config.headers) @@ -1190,13 +1404,8 @@ async def setup_context( "downloads_path" ] = self.config.downloads_path - # Handle user agent and browser hints - if self.config.user_agent: - combined_headers = { - "User-Agent": self.config.user_agent, - "sec-ch-ua": self.config.browser_hint, - } - combined_headers.update(self.config.headers) + combined_headers = self._combined_browser_headers() + if combined_headers: await context.set_extra_http_headers(combined_headers) # Add default cookie (skip for raw:/file:// URLs which are not valid cookie URLs) @@ -1222,14 +1431,9 @@ async def setup_context( ) # Handle navigator overrides - if crawlerRunConfig: - if ( - crawlerRunConfig.override_navigator - or crawlerRunConfig.simulate_user - or crawlerRunConfig.magic - ): - await context.add_init_script(load_js_script("navigator_overrider")) - context._crawl4ai_nav_overrider_injected = True + if self._should_inject_navigator_overrides(crawlerRunConfig): + await context.add_init_script(load_js_script("navigator_overrider")) + context._crawl4ai_nav_overrider_injected = True # Force-open closed shadow roots when flatten_shadow_dom is enabled if crawlerRunConfig and crawlerRunConfig.flatten_shadow_dom: @@ -1266,13 +1470,7 @@ async def create_browser_context(self, crawlerRunConfig: CrawlerRunConfig = None "or not yet started. Ensure the browser is running before " "creating new contexts." ) - # Base settings - user_agent = self.config.headers.get("User-Agent", self.config.user_agent) - viewport_settings = { - "width": self.config.viewport_width, - "height": self.config.viewport_height, - } - proxy_settings = {"server": self.config.proxy} if self.config.proxy else None + self.config.validate_crawler_run_config(crawlerRunConfig) # CSS extensions (blocked separately via avoid_css flag) css_extensions = ["css", "less", "scss", "sass"] @@ -1317,55 +1515,7 @@ async def create_browser_context(self, crawlerRunConfig: CrawlerRunConfig = None "**/segment.com/**", ] - # Common context settings - context_settings = { - "user_agent": user_agent, - "viewport": viewport_settings, - "proxy": proxy_settings, - "accept_downloads": self.config.accept_downloads, - "storage_state": self.config.storage_state, - "ignore_https_errors": self.config.ignore_https_errors, - "device_scale_factor": self.config.device_scale_factor, - "java_script_enabled": self.config.java_script_enabled, - } - - if crawlerRunConfig: - # Check if there is value for crawlerRunConfig.proxy_config set add that to context - if crawlerRunConfig.proxy_config: - from playwright.async_api import ProxySettings - proxy_settings = ProxySettings( - server=crawlerRunConfig.proxy_config.server, - username=crawlerRunConfig.proxy_config.username, - password=crawlerRunConfig.proxy_config.password, - ) - context_settings["proxy"] = proxy_settings - - if self.config.text_mode: - text_mode_settings = { - "has_touch": False, - "is_mobile": False, - } - # Update context settings with text mode settings - context_settings.update(text_mode_settings) - - # inject locale / tz / geo if user provided them - if crawlerRunConfig: - if crawlerRunConfig.locale: - context_settings["locale"] = crawlerRunConfig.locale - if crawlerRunConfig.timezone_id: - context_settings["timezone_id"] = crawlerRunConfig.timezone_id - if crawlerRunConfig.geolocation: - context_settings["geolocation"] = { - "latitude": crawlerRunConfig.geolocation.latitude, - "longitude": crawlerRunConfig.geolocation.longitude, - "accuracy": crawlerRunConfig.geolocation.accuracy, - } - # ensure geolocation permission - perms = context_settings.get("permissions", []) - perms.append("geolocation") - context_settings["permissions"] = perms - - # Create and return the context with all settings + context_settings = self._build_context_settings(crawlerRunConfig) context = await self.browser.new_context(**context_settings) # Build dynamic blocking list based on config flags @@ -1395,10 +1545,15 @@ def _make_config_signature(self, crawlerRunConfig: CrawlerRunConfig) -> str: """ import json + self.config.validate_crawler_run_config(crawlerRunConfig) sig_dict = {} # Fields that flow into create_browser_context() - pc = crawlerRunConfig.proxy_config + pc = ( + crawlerRunConfig.proxy_config + if not self.config.uses_browser_scoped_identity + else None + ) if pc is not None: sig_dict["proxy_config"] = { "server": getattr(pc, "server", None), @@ -1408,10 +1563,22 @@ def _make_config_signature(self, crawlerRunConfig: CrawlerRunConfig) -> str: else: sig_dict["proxy_config"] = None - sig_dict["locale"] = crawlerRunConfig.locale - sig_dict["timezone_id"] = crawlerRunConfig.timezone_id + sig_dict["locale"] = ( + crawlerRunConfig.locale + if not self.config.uses_browser_scoped_identity + else None + ) + sig_dict["timezone_id"] = ( + crawlerRunConfig.timezone_id + if not self.config.uses_browser_scoped_identity + else None + ) - geo = crawlerRunConfig.geolocation + geo = ( + crawlerRunConfig.geolocation + if not self.config.uses_browser_scoped_identity + else None + ) if geo is not None: sig_dict["geolocation"] = { "latitude": geo.latitude, @@ -1422,9 +1589,19 @@ def _make_config_signature(self, crawlerRunConfig: CrawlerRunConfig) -> str: sig_dict["geolocation"] = None # Fields that flow into setup_context() as init scripts - sig_dict["override_navigator"] = crawlerRunConfig.override_navigator - sig_dict["simulate_user"] = crawlerRunConfig.simulate_user - sig_dict["magic"] = crawlerRunConfig.magic + sig_dict["override_navigator"] = self._should_inject_navigator_overrides( + crawlerRunConfig + ) + sig_dict["simulate_user"] = ( + crawlerRunConfig.simulate_user + if not self.config.uses_browser_scoped_identity + else False + ) + sig_dict["magic"] = ( + crawlerRunConfig.magic + if not self.config.uses_browser_scoped_identity + else False + ) # Browser version — bumped on recycle to force new browser instance sig_dict["_browser_version"] = self._browser_version @@ -1545,6 +1722,7 @@ async def get_page(self, crawlerRunConfig: CrawlerRunConfig): Returns: (page, context): The Page and its BrowserContext """ + self.config.validate_crawler_run_config(crawlerRunConfig) self._cleanup_expired_sessions() # If a session_id is provided and we already have it, reuse that page + context @@ -2027,6 +2205,27 @@ async def close(self): self.playwright = None return + if self.config.is_camoufox: + session_ids = list(self.sessions.keys()) + for session_id in session_ids: + await self.kill_session(session_id) + + for ctx in self.contexts_by_config.values(): + try: + await ctx.close() + except Exception: + pass + self.contexts_by_config.clear() + self._context_refcounts.clear() + self._context_last_used.clear() + self._page_to_sig.clear() + + await self.runtime_backend.close() + self.browser = None + self.default_context = None + self._launched_persistent = False + return + # ── Persistent context launched via launch_persistent_context ── if self._launched_persistent: session_ids = list(self.sessions.keys()) diff --git a/docs/examples/camoufox_example.py b/docs/examples/camoufox_example.py new file mode 100644 index 000000000..426f11492 --- /dev/null +++ b/docs/examples/camoufox_example.py @@ -0,0 +1,49 @@ +import asyncio + +from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig + + +async def main(): + dedicated_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + headless=True, + camoufox_options={ + "geoip": True, + "humanize": True, + }, + ) + + async with AsyncWebCrawler(config=dedicated_config) as crawler: + result = await crawler.arun( + "https://example.com", + config=CrawlerRunConfig(wait_for="css:body"), + ) + print("dedicated:", result.success, len(result.html)) + + persistent_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + use_persistent_context=True, + user_data_dir="./camoufox-profile", + proxy_config={ + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass", + }, + camoufox_options={ + "geoip": True, + "humanize": True, + }, + ) + + async with AsyncWebCrawler(config=persistent_config) as crawler: + result = await crawler.arun( + "https://example.com/account", + config=CrawlerRunConfig(), + ) + print("persistent:", result.success, len(result.html)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/examples/camoufox_proxy_example.py b/docs/examples/camoufox_proxy_example.py new file mode 100644 index 000000000..2cbd57853 --- /dev/null +++ b/docs/examples/camoufox_proxy_example.py @@ -0,0 +1,56 @@ +import asyncio +import os +import platform + +from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig + + +def parse_webshare_proxy(proxy_line: str) -> dict[str, str]: + host, port, username, password = proxy_line.strip().split(":", 3) + return { + "server": f"http://{host}:{port}", + "username": username, + "password": password, + } + + +def build_camoufox_options() -> dict[str, object]: + return { + "headless": "virtual" if platform.system() == "Linux" else True, + "geoip": True, + "humanize": True, + } + + +async def main(): + proxy_line = os.environ.get("WEBSHARE_PROXY") + if not proxy_line: + raise RuntimeError( + "Set WEBSHARE_PROXY to a Webshare line formatted as " + "host:port:username:password" + ) + + browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + use_persistent_context=True, + user_data_dir=os.environ.get("CAMOUFOX_PROFILE_DIR", "./camoufox-profile"), + proxy_config=parse_webshare_proxy(proxy_line), + camoufox_options=build_camoufox_options(), + ) + + run_config = CrawlerRunConfig( + wait_for="css:body", + wait_until="domcontentloaded", + ) + + async with AsyncWebCrawler(config=browser_config) as crawler: + target_url = os.environ.get("CAMOUFOX_TARGET_URL", "https://example.com") + result = await crawler.arun(target_url, config=run_config) + print("success:", result.success) + print("url:", result.url) + print("html_length:", len(result.html)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/md_v2/advanced/camoufox-browser.md b/docs/md_v2/advanced/camoufox-browser.md new file mode 100644 index 000000000..014bd2b08 --- /dev/null +++ b/docs/md_v2/advanced/camoufox-browser.md @@ -0,0 +1,200 @@ +# Camoufox Runtime + +## Overview + +Crawl4AI now supports Camoufox as a first-class browser runtime through `BrowserConfig(browser_runtime="camoufox")`. + +This keeps the normal Crawl4AI surface intact: + +- `AsyncWebCrawler` +- `CrawlerRunConfig` +- extraction and markdown generation +- screenshots and waiting primitives +- session reuse +- persistent browser profiles + +Camoufox support is intentionally **local-first** in v1: + +- supported: dedicated local launch +- supported: persistent local context +- not yet supported in Crawl4AI: CDP/custom browser mode +- not yet supported in Crawl4AI: Camoufox remote WebSocket mode + +## Installation + +Install the optional Crawl4AI extra or Camoufox directly: + +```bash +pip install "crawl4ai[camoufox]" +``` + +Or: + +```bash +pip install -U camoufox[geoip] +python -m camoufox fetch +``` + +## Linux / VPS Notes + +For Linux and VPS deployments, prefer Camoufox's virtual display mode instead of plain headless mode. Camoufox's own docs recommend installing `xvfb` and using `headless="virtual"` when you want headless execution on Linux: + +```bash +sudo apt-get install xvfb +``` + +Then use: + +```python +camoufox_options = { + "headless": "virtual", + "geoip": True, + "humanize": True, +} +``` + +## Basic Usage + +```python +from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig + +browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + headless=True, + camoufox_options={ + "geoip": True, + "humanize": True, + }, +) + +async with AsyncWebCrawler(config=browser_config) as crawler: + result = await crawler.arun( + "https://example.com", + config=CrawlerRunConfig(wait_for="css:body"), + ) + print(result.success) + print(result.markdown[:500]) +``` + +## Persistent Context Example + +```python +from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig + +browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + use_persistent_context=True, + user_data_dir="./camoufox-profile", + camoufox_options={ + "geoip": True, + "humanize": True, + }, +) + +async with AsyncWebCrawler(config=browser_config) as crawler: + result = await crawler.arun( + "https://example.com/account", + config=CrawlerRunConfig(), + ) + print(result.success) +``` + +## Identity Rules + +Camoufox owns browser identity at the **browser session** level. + +That means: + +- use `camoufox_options` for browser fingerprint and geo settings +- use `BrowserConfig.proxy_config` for the browser proxy +- do not use per-run identity overrides with `CrawlerRunConfig` + +These `CrawlerRunConfig` fields are rejected in Camoufox mode: + +- `proxy_config` +- `user_agent` +- `user_agent_mode` +- `locale` +- `timezone_id` +- `geolocation` +- `override_navigator` +- `magic` + +In `BrowserConfig.headers`, avoid identity-bearing headers such as: + +- `User-Agent` +- `Accept-Language` +- `sec-ch-ua*` + +## Residential Proxies + +Camoufox works best when the proxy and browser identity are configured together at browser-launch time. + +If your provider exports proxies in Webshare's `host:port:username:password` format, convert one line into `BrowserConfig.proxy_config` before you create the crawler: + +```python +def parse_webshare_proxy(proxy_line: str) -> dict[str, str]: + host, port, username, password = proxy_line.strip().split(":", 3) + return { + "server": f"http://{host}:{port}", + "username": username, + "password": password, + } +``` + +Then use it like this: + +```python +browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + proxy_config=parse_webshare_proxy(proxy_line), + camoufox_options={ + "headless": "virtual", + "geoip": True, + "humanize": True, + }, +) +``` + +Pick one residential proxy per browser session. Camoufox mode rejects per-run proxy overrides because the browser fingerprint, locale, timezone, and WebRTC state should all stay aligned with the chosen exit IP. + +See `docs/examples/camoufox_proxy_example.py` for a complete environment-driven example that works on local machines and Linux hosts. + +## Compatibility Notes + +- `browser_type` must be `"firefox"` when `browser_runtime="camoufox"`. +- `browser_mode` must stay `"dedicated"`. +- `enable_stealth=True` is not allowed with Camoufox. +- `UndetectedAdapter` is not allowed with Camoufox. +- `storage_state` is not supported together with `use_persistent_context=True`; use `user_data_dir` to reuse session state instead. +- Camoufox persistent contexts are the recommended way to reuse sessions and browser state. + +## Recommended Starting Point + +Use this as the first production-style config: + +```python +browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + use_persistent_context=True, + user_data_dir="./camoufox-profile", + proxy_config={ + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass", + }, + camoufox_options={ + "geoip": True, + "humanize": True, + }, +) +``` + +## Example Files + +- `docs/examples/camoufox_example.py` demonstrates dedicated and persistent Camoufox sessions. +- `docs/examples/camoufox_proxy_example.py` shows how to pair Camoufox with a residential proxy using environment variables. diff --git a/docs/md_v2/core/browser-crawler-config.md b/docs/md_v2/core/browser-crawler-config.md index d9946c689..713968ff4 100644 --- a/docs/md_v2/core/browser-crawler-config.md +++ b/docs/md_v2/core/browser-crawler-config.md @@ -15,6 +15,7 @@ In most examples, you create **one** `BrowserConfig` for the entire crawler sess ```python class BrowserConfig: def __init__( + browser_runtime="playwright", browser_type="chromium", headless=True, browser_mode="dedicated", @@ -30,13 +31,9 @@ class BrowserConfig: user_data_dir=None, cookies=None, headers=None, - user_agent=( - # "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) AppleWebKit/537.36 " - # "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " - # "(KHTML, like Gecko) Chrome/116.0.5845.187 Safari/604.1 Edg/117.0.2045.47" - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/116.0.0.0 Safari/537.36" - ), + user_agent=None, user_agent_mode="", + camoufox_options=None, text_mode=False, light_mode=False, extra_args=None, @@ -48,32 +45,39 @@ class BrowserConfig: ### Key Fields to Note -1.⠀**`browser_type`** +1.⠀**`browser_runtime`** + - Options: `"playwright"` (default) or `"camoufox"`. + - Use `"camoufox"` for Firefox-based Camoufox sessions. + - Camoufox support is local-first in v1: dedicated launch and persistent contexts only. + +2.⠀**`browser_type`** - Options: `"chromium"`, `"firefox"`, or `"webkit"`. - Defaults to `"chromium"`. - - If you need a different engine, specify it here. + - If `browser_runtime="camoufox"`, `browser_type` must be `"firefox"`. -2.⠀**`headless`** +3.⠀**`headless`** - `True`: Runs the browser in headless mode (invisible browser). - `False`: Runs the browser in visible mode, which helps with debugging. -3.⠀**`browser_mode`** +4.⠀**`browser_mode`** - Determines how the browser should be initialized: - `"dedicated"` (default): Creates a new browser instance each time - `"builtin"`: Uses the builtin CDP browser running in background - `"custom"`: Uses explicit CDP settings provided in `cdp_url` - `"docker"`: Runs browser in Docker container with isolation + - Camoufox currently supports only `"dedicated"`. -4.⠀**`use_managed_browser`** & **`cdp_url`** +5.⠀**`use_managed_browser`** & **`cdp_url`** - `use_managed_browser=True`: Launch browser using Chrome DevTools Protocol (CDP) for advanced control - `cdp_url`: URL for CDP endpoint (e.g., `"ws://localhost:9222/devtools/browser/"`) - Automatically set based on `browser_mode` + - Camoufox does not support Crawl4AI's CDP/custom browser path in v1. -5.⠀**`debugging_port`** & **`host`** +6.⠀**`debugging_port`** & **`host`** - `debugging_port`: Port for browser debugging protocol (default: 9222) - `host`: Host for browser connection (default: "localhost") -6.⠀**`proxy_config`** +7.⠀**`proxy_config`** - A `ProxyConfig` object or dictionary with fields like: ```json { @@ -84,48 +88,57 @@ class BrowserConfig: ``` - Leave as `None` if a proxy is not required. -7.⠀**`viewport_width` & `viewport_height`** +8.⠀**`viewport_width` & `viewport_height`** - The initial window size. - Some sites behave differently with smaller or bigger viewports. -8.⠀**`device_scale_factor`** +9.⠀**`device_scale_factor`** - Controls the device pixel ratio (DPR) for rendering. Default is `1.0`. - Set to `2.0` for Retina-quality screenshots (e.g., a 1920×1080 viewport produces 3840×2160 images). - Higher values increase screenshot size and rendering time proportionally. -9.⠀**`verbose`** +10.⠀**`verbose`** - If `True`, prints extra logs. - Handy for debugging. -9.⠀**`use_persistent_context`** +11.⠀**`use_persistent_context`** - If `True`, uses a **persistent** browser profile, storing cookies/local storage across runs. - Typically also set `user_data_dir` to point to a folder. + - In Camoufox mode, prefer `user_data_dir` over `storage_state` for persistent sessions. -10.⠀**`cookies`** & **`headers`** +12.⠀**`cookies`** & **`headers`** - If you want to start with specific cookies or add universal HTTP headers to the browser context, set them here. - E.g. `cookies=[{"name": "session", "value": "abc123", "domain": "example.com"}]`. + - In Camoufox mode, only non-identity headers are allowed here. -11.⠀**`user_agent`** & **`user_agent_mode`** +13.⠀**`user_agent`** & **`user_agent_mode`** - `user_agent`: Custom User-Agent string. If `None`, a default is used. - `user_agent_mode`: Set to `"random"` for randomization (helps fight bot detection). + - In Camoufox mode, keep identity settings inside `camoufox_options` instead of `user_agent` or `user_agent_mode`. + +14.⠀**`camoufox_options`** + - Optional dict of Camoufox-specific launch and fingerprint settings. + - Good home for options like `geoip`, `humanize`, `os`, `screen`, `window`, and browser-scope proxy/fingerprint tuning. + - See the dedicated [Camoufox guide](../advanced/camoufox-browser.md) for the recommended setup. -12.⠀**`text_mode`** & **`light_mode`** +15.⠀**`text_mode`** & **`light_mode`** - `text_mode=True` disables images, possibly speeding up text-only crawls. - `light_mode=True` turns off certain background features for performance. -13.⠀**`avoid_ads`** & **`avoid_css`** +16.⠀**`avoid_ads`** & **`avoid_css`** - `avoid_ads=True` blocks requests to common ad and tracker domains (Google Analytics, DoubleClick, Facebook, Hotjar, etc.) at the browser context level. Reduces network overhead and memory usage. - `avoid_css=True` blocks loading of CSS files (`.css`, `.less`, `.scss`, `.sass`), useful when you only need text content and want faster, leaner crawls. - Both default to `False` (opt-in). Can be combined with each other and with `text_mode`. -14.⠀**`extra_args`** +17.⠀**`extra_args`** - Additional flags for the underlying browser. - E.g. `["--disable-extensions"]`. -15.⠀**`enable_stealth`** +18.⠀**`enable_stealth`** - If `True`, enables stealth mode using playwright-stealth. - Modifies browser fingerprints to avoid basic bot detection. - Default is `False`. Recommended for sites with bot protection. + - Do not combine this with Camoufox. Camoufox owns stealth and fingerprint behavior itself. ### Helper Methods @@ -484,4 +497,4 @@ You can explore topics like: - **How** each crawl should behave—caching, timeouts, JavaScript code, extraction strategies, etc. - **Which** LLM provider to use, api token, temperature and base url for custom endpoints -Use them together for **clear, maintainable** code, and when you need more specialized behavior, check out the advanced parameters in the [reference docs](../api/parameters.md). Happy crawling! \ No newline at end of file +Use them together for **clear, maintainable** code, and when you need more specialized behavior, check out the advanced parameters in the [reference docs](../api/parameters.md). Happy crawling! diff --git a/docs/md_v2/core/examples.md b/docs/md_v2/core/examples.md index d5d58e025..18a393a63 100644 --- a/docs/md_v2/core/examples.md +++ b/docs/md_v2/core/examples.md @@ -23,6 +23,7 @@ This page provides a comprehensive list of example scripts that demonstrate vari |---------|-------------|------| | Built-in Browser | Demonstrates how to use the built-in browser capabilities. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/builtin_browser_example.py) | | Browser Optimization | Focuses on browser performance optimization techniques. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/browser_optimization_example.py) | +| Camoufox Runtime | Shows how to use Crawl4AI with Camoufox in dedicated and persistent modes. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/camoufox_example.py) | | arun vs arun_many | Compares the `arun` and `arun_many` methods for single vs. multiple URL crawling. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/arun_vs_arun_many.py) | | Multiple URLs | Shows how to crawl multiple URLs asynchronously. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/async_webcrawler_multiple_urls_example.py) | | Page Interaction | Guide on interacting with dynamic elements through clicks. | [View Guide](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/tutorial_dynamic_clicks.md) | @@ -77,6 +78,7 @@ This page provides a comprehensive list of example scripts that demonstrate vari |---------|-------------|------| | Hooks | Illustrates how to use hooks at different stages of the crawling process for advanced customization. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/hooks_example.py) | | Identity-Based Browsing | Illustrates identity-based browsing configurations for authentic browsing experiences. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/identity_based_browsing.py) | +| Camoufox Proxy Example | Shows how to launch Camoufox with a residential proxy and a persistent profile. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/camoufox_proxy_example.py) | | Proxy Rotation | Shows how to use proxy rotation for web scraping and avoiding IP blocks. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/proxy_rotation_demo.py) | | SSL Certificate | Illustrates SSL certificate handling and verification. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/ssl_example.py) | | Language Support | Shows how to handle different languages during crawling. | [View Code](https://github.com/unclecode/crawl4ai/blob/main/docs/examples/language_support_example.py) | diff --git a/mkdocs.yml b/mkdocs.yml index 1dee32f96..029e93e67 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,7 @@ nav: - "Proxy & Security": "advanced/proxy-security.md" - "Anti-Bot & Fallback": "advanced/anti-bot-and-fallback.md" - "Undetected Browser": "advanced/undetected-browser.md" + - "Camoufox Runtime": "advanced/camoufox-browser.md" - "Session Management": "advanced/session-management.md" - "Multi-URL Crawling": "advanced/multi-url-crawling.md" - "Crawl Dispatcher": "advanced/crawl-dispatcher.md" @@ -121,4 +122,4 @@ extra_javascript: - assets/copy_code.js - assets/floating_ask_ai_button.js - assets/mobile_menu.js - - assets/page_actions.js?v=20251006 \ No newline at end of file + - assets/page_actions.js?v=20251006 diff --git a/pyproject.toml b/pyproject.toml index ee237d5b3..7fc3516b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ pdf = ["pypdf"] torch = ["torch", "nltk", "scikit-learn"] transformer = ["transformers", "tokenizers", "sentence-transformers"] cosine = ["torch", "transformers", "nltk", "sentence-transformers"] +camoufox = ["camoufox[geoip]>=0.4.0"] sync = ["selenium"] all = [ "pypdf", @@ -72,7 +73,8 @@ all = [ "transformers", "tokenizers", "sentence-transformers", - "selenium" + "selenium", + "camoufox[geoip]>=0.4.0" ] [project.scripts] diff --git a/tests/regression/test_reg_camoufox.py b/tests/regression/test_reg_camoufox.py new file mode 100644 index 000000000..48309b665 --- /dev/null +++ b/tests/regression/test_reg_camoufox.py @@ -0,0 +1,126 @@ +import pytest + +from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig +from crawl4ai.async_logger import AsyncLogger +from crawl4ai.browser_manager import BrowserManager + + +def _skip_if_camoufox_unavailable(): + pytest.importorskip("camoufox.async_api") + + +def _skip_if_camoufox_runtime_missing(exc: Exception) -> None: + message = str(exc).lower() + environment_hints = ( + "camoufox fetch", + "browser executable", + "no such file or directory", + "cannot open display", + "xvfb", + "shared object file", + "libgtk", + ) + if isinstance(exc, (FileNotFoundError, OSError)) or any( + hint in message for hint in environment_hints + ): + pytest.skip(f"Camoufox runtime is not ready on this machine: {exc}") + raise exc + + +@pytest.mark.asyncio +async def test_camoufox_static_crawl(local_server): + _skip_if_camoufox_unavailable() + browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + headless=True, + ) + + try: + async with AsyncWebCrawler( + config=browser_config, + logger=AsyncLogger(verbose=False), + ) as crawler: + result = await crawler.arun( + f"{local_server}/products", + config=CrawlerRunConfig(screenshot=True), + ) + except Exception as exc: + _skip_if_camoufox_runtime_missing(exc) + + assert result.success is True + assert "Product" in result.html + assert result.screenshot is not None + + +@pytest.mark.asyncio +async def test_camoufox_js_dynamic_crawl(local_server): + _skip_if_camoufox_unavailable() + browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + headless=True, + ) + + try: + async with AsyncWebCrawler( + config=browser_config, + logger=AsyncLogger(verbose=False), + ) as crawler: + result = await crawler.arun( + f"{local_server}/js-dynamic", + config=CrawlerRunConfig(wait_for="css:.js-loaded"), + ) + except Exception as exc: + _skip_if_camoufox_runtime_missing(exc) + + assert result.success is True + assert "Dynamic content successfully loaded via JavaScript" in result.html + + +@pytest.mark.asyncio +async def test_camoufox_persistent_context_reuses_local_storage(local_server, tmp_path): + _skip_if_camoufox_unavailable() + profile_dir = tmp_path / "camoufox-profile" + browser_config = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + headless=True, + use_persistent_context=True, + user_data_dir=str(profile_dir), + ) + + manager = None + manager_2 = None + try: + manager = BrowserManager( + browser_config=browser_config, + logger=AsyncLogger(verbose=False), + ) + await manager.start() + page, _ = await manager.get_page(CrawlerRunConfig(url=f"{local_server}/")) + await page.goto(f"{local_server}/", wait_until="domcontentloaded") + await page.evaluate( + "localStorage.setItem('camoufox-persistent-key', 'camoufox-value')" + ) + await manager.close() + + manager_2 = BrowserManager( + browser_config=browser_config, + logger=AsyncLogger(verbose=False), + ) + await manager_2.start() + page_2, _ = await manager_2.get_page(CrawlerRunConfig(url=f"{local_server}/")) + await page_2.goto(f"{local_server}/", wait_until="domcontentloaded") + persisted_value = await page_2.evaluate( + "localStorage.getItem('camoufox-persistent-key')" + ) + except Exception as exc: + _skip_if_camoufox_runtime_missing(exc) + finally: + if manager is not None: + await manager.close() + if manager_2 is not None: + await manager_2.close() + + assert persisted_value == "camoufox-value" diff --git a/tests/unit/test_camoufox_runtime.py b/tests/unit/test_camoufox_runtime.py new file mode 100644 index 000000000..3d4f8f85c --- /dev/null +++ b/tests/unit/test_camoufox_runtime.py @@ -0,0 +1,321 @@ +import types + +import pytest + +from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy +from crawl4ai.async_logger import AsyncLogger +from crawl4ai.browser_adapter import UndetectedAdapter +import crawl4ai.browser_manager as browser_manager_module +from crawl4ai.browser_manager import BrowserManager + + +class FakeContext: + def __init__(self): + self._impl_obj = types.SimpleNamespace(_options={}) + self.headers = [] + self.cookies = [] + self.init_scripts = [] + self.closed = False + self.pages = [] + + async def set_extra_http_headers(self, headers): + self.headers.append(dict(headers)) + + async def add_cookies(self, cookies): + self.cookies.extend(cookies) + + async def storage_state(self, path=None): + return {"cookies": [], "origins": []} + + def set_default_timeout(self, value): + self.default_timeout = value + + def set_default_navigation_timeout(self, value): + self.default_navigation_timeout = value + + async def add_init_script(self, script): + self.init_scripts.append(script) + + async def close(self): + self.closed = True + + +class FakeBrowser: + def __init__(self): + self.created_contexts = [] + self.closed = False + + async def new_context(self, **kwargs): + context = FakeContext() + context.context_kwargs = kwargs + self.created_contexts.append(context) + return context + + async def close(self): + self.closed = True + + +class FakeAsyncCamoufox: + instances = [] + + def __init__(self, **kwargs): + self.kwargs = kwargs + self.entered = False + self.exited = False + self.result = None + type(self).instances.append(self) + + async def __aenter__(self): + self.entered = True + if self.kwargs.get("persistent_context"): + self.result = FakeContext() + else: + self.result = FakeBrowser() + return self.result + + async def __aexit__(self, exc_type, exc, tb): + self.exited = True + + +@pytest.fixture(autouse=True) +def reset_fake_instances(): + FakeAsyncCamoufox.instances.clear() + yield + FakeAsyncCamoufox.instances.clear() + + +def install_fake_camoufox(monkeypatch): + real_import_module = browser_manager_module.importlib.import_module + + def fake_import_module(name, *args, **kwargs): + if name == "camoufox.async_api": + return types.SimpleNamespace(AsyncCamoufox=FakeAsyncCamoufox) + return real_import_module(name, *args, **kwargs) + + monkeypatch.setattr( + browser_manager_module.importlib, "import_module", fake_import_module + ) + + +def test_firefox_defaults_do_not_inherit_chrome_identity(): + cfg = BrowserConfig(browser_type="firefox") + + assert "Firefox/" in cfg.user_agent + assert "Chrome/" not in cfg.user_agent + assert "sec-ch-ua" not in cfg.headers + assert cfg.browser_hint == "" + + +def test_chromium_defaults_stay_unchanged(): + cfg = BrowserConfig() + + assert "Chrome/" in cfg.user_agent + assert cfg.headers["sec-ch-ua"] + + +@pytest.mark.parametrize("browser_type", ["chromium", "webkit"]) +def test_camoufox_requires_firefox(browser_type): + with pytest.raises(ValueError, match="requires browser_type='firefox'"): + BrowserConfig(browser_runtime="camoufox", browser_type=browser_type) + + +@pytest.mark.parametrize("browser_mode", ["builtin", "custom", "docker", "cdp"]) +def test_camoufox_requires_dedicated_browser_mode(browser_mode): + with pytest.raises(ValueError, match="supports only browser_mode='dedicated'"): + BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + browser_mode=browser_mode, + cdp_url="ws://localhost:9222/devtools/browser/example" + if browser_mode in {"custom", "cdp"} + else None, + ) + + +def test_camoufox_rejects_cdp_url_even_in_dedicated_mode(): + with pytest.raises(ValueError, match="does not support cdp_url"): + BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + cdp_url="ws://localhost:9222/devtools/browser/example", + ) + + +def test_camoufox_persistent_context_rejects_storage_state(): + with pytest.raises(ValueError, match="does not support storage_state"): + BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + use_persistent_context=True, + user_data_dir="/tmp/camoufox-profile", + storage_state={"cookies": [], "origins": []}, + ) + + +def test_camoufox_rejects_conflicting_proxy_sources(): + with pytest.raises(ValueError, match="Set only one proxy source"): + BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + proxy_config={"server": "http://proxy.example:8080"}, + camoufox_options={"proxy": {"server": "http://other.example:8080"}}, + ) + + +def test_camoufox_rejects_identity_headers(): + with pytest.raises(ValueError, match="identity-bearing BrowserConfig.headers"): + BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + headers={"Accept-Language": "en-US,en;q=0.9"}, + ) + + +def test_camoufox_rejects_enable_stealth(): + with pytest.raises(ValueError, match="cannot be combined with enable_stealth"): + BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + enable_stealth=True, + ) + + +def test_camoufox_roundtrip_preserves_runtime_config(): + cfg = BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + camoufox_options={"geoip": True, "humanize": True}, + ) + + restored = BrowserConfig.from_kwargs(cfg.to_dict()) + cloned = cfg.clone() + + assert restored.browser_runtime == "camoufox" + assert restored.camoufox_options == {"geoip": True, "humanize": True} + assert cloned.browser_runtime == "camoufox" + + +@pytest.mark.parametrize( + "field_name, field_value", + [ + ("proxy_config", {"server": "http://proxy.example:8080"}), + ("user_agent", "Mozilla/5.0 Custom"), + ("user_agent_mode", "random"), + ("locale", "en-US"), + ("timezone_id", "UTC"), + ("geolocation", {"latitude": 1.0, "longitude": 2.0, "accuracy": 3.0}), + ("override_navigator", True), + ("magic", True), + ], +) +def test_camoufox_rejects_run_level_identity_overrides(field_name, field_value): + cfg = BrowserConfig(browser_runtime="camoufox", browser_type="firefox") + run_cfg = CrawlerRunConfig(**{field_name: field_value}) + + with pytest.raises(ValueError, match="Remove these CrawlerRunConfig overrides"): + cfg.validate_crawler_run_config(run_cfg) + + +def test_camoufox_rejects_undetected_adapter(): + with pytest.raises(ValueError, match="cannot be combined with UndetectedAdapter"): + AsyncPlaywrightCrawlerStrategy( + browser_config=BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + ), + logger=AsyncLogger(verbose=False), + browser_adapter=UndetectedAdapter(), + ) + + +def test_playwright_firefox_launch_args_skip_chromium_flags(): + manager = BrowserManager( + browser_config=BrowserConfig(browser_type="firefox"), + logger=AsyncLogger(verbose=False), + ) + + browser_args = manager._build_browser_args() + + assert browser_args["headless"] is True + assert browser_args.get("args", []) == [] + assert "channel" not in browser_args + + +def test_camoufox_launch_args_include_camoufox_options_and_proxy(): + manager = BrowserManager( + browser_config=BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + proxy_config={"server": "http://proxy.example:8080"}, + camoufox_options={"geoip": True, "humanize": True}, + ), + logger=AsyncLogger(verbose=False), + ) + + browser_args = manager._build_browser_args() + + assert browser_args["headless"] is True + assert browser_args["geoip"] is True + assert browser_args["humanize"] is True + assert browser_args["proxy"]["server"] == "http://proxy.example:8080" + + +@pytest.mark.asyncio +async def test_camoufox_browser_manager_dedicated_launch_and_cleanup(monkeypatch): + install_fake_camoufox(monkeypatch) + manager = BrowserManager( + browser_config=BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + proxy_config={"server": "http://proxy.example:8080"}, + camoufox_options={"geoip": True}, + ), + logger=AsyncLogger(verbose=False), + ) + + await manager.start() + + instance = FakeAsyncCamoufox.instances[-1] + assert instance.entered is True + assert instance.kwargs["geoip"] is True + assert instance.kwargs["proxy"]["server"] == "http://proxy.example:8080" + assert manager.browser is instance.result + assert manager.default_context is manager.browser + + await manager.close() + + assert instance.exited is True + + +@pytest.mark.asyncio +async def test_camoufox_browser_manager_persistent_launch_and_cleanup( + monkeypatch, tmp_path +): + install_fake_camoufox(monkeypatch) + profile_dir = tmp_path / "camoufox-profile" + manager = BrowserManager( + browser_config=BrowserConfig( + browser_runtime="camoufox", + browser_type="firefox", + use_persistent_context=True, + user_data_dir=str(profile_dir), + camoufox_options={"geoip": True, "humanize": True}, + ), + logger=AsyncLogger(verbose=False), + ) + + await manager.start() + + instance = FakeAsyncCamoufox.instances[-1] + assert instance.entered is True + assert instance.kwargs["persistent_context"] is True + assert instance.kwargs["user_data_dir"] == str(profile_dir) + assert instance.kwargs["geoip"] is True + assert instance.kwargs["humanize"] is True + assert manager.browser is None + assert manager.default_context is instance.result + + await manager.close() + + assert instance.exited is True diff --git a/uv.lock b/uv.lock index 50ce22807..b433751cf 100644 --- a/uv.lock +++ b/uv.lock @@ -178,6 +178,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] +[[package]] +name = "apify-fingerprint-datapoints" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/352bf34db5c4df1941ee10feade33009f05f2958a4b771b829bfcd0cdd41/apify_fingerprint_datapoints-0.12.0.tar.gz", hash = "sha256:a748d6cf2cee853f0276421e661d398cf725e7f453a1a8228e11a3b28db1d825", size = 887530, upload-time = "2026-04-01T01:08:28.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/ca/6de5a2ab007751debedf6ab48c3784cc9e6c9a7c433adb08d89c26448fce/apify_fingerprint_datapoints-0.12.0-py3-none-any.whl", hash = "sha256:27dae89b5d21710d96ec23d00bbc64e7d2381316546d41da6aa5ebab65f151b9", size = 643752, upload-time = "2026-04-01T01:08:26.545Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -279,6 +288,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload-time = "2024-10-18T12:32:54.066Z" }, ] +[[package]] +name = "browserforge" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apify-fingerprint-datapoints" }, + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/6f/8975af88d203efd70cc69477ebac702babef38201d04621c9583f2508f25/browserforge-1.2.4.tar.gz", hash = "sha256:05686473793769856ebd3528c69071f5be0e511260993e8b2ba839863711a0c4", size = 36700, upload-time = "2026-02-03T02:52:09.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/35/ce962f738ae28ffce6293e7607b129075633e6bb185a5ab87e49246eedc2/browserforge-1.2.4-py3-none-any.whl", hash = "sha256:fb1c14e62ac09de221dcfc73074200269f697596c642cb200ceaab1127a17542", size = 37890, upload-time = "2026-02-03T02:52:08.745Z" }, +] + +[[package]] +name = "camoufox" +version = "0.4.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "browserforge" }, + { name = "click" }, + { name = "language-tags" }, + { name = "lxml" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "platformdirs" }, + { name = "playwright" }, + { name = "pysocks" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "screeninfo" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "ua-parser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/15/e0a1b586e354ea6b8d6612717bf4372aaaa6753444d5d006caf0bb116466/camoufox-0.4.11.tar.gz", hash = "sha256:0a2c9d24ac5070c104e7c2b125c0a3937f70efa416084ef88afe94c32a72eebe", size = 64409, upload-time = "2025-01-29T09:33:20.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/7b/a2f099a5afb9660271b3f20f6056ba679e7ab4eba42682266a65d5730f7e/camoufox-0.4.11-py3-none-any.whl", hash = "sha256:83864d434d159a7566990aa6524429a8d1a859cbf84d2f64ef4a9f29e7d2e5ff", size = 71628, upload-time = "2025-01-29T09:33:18.558Z" }, +] + +[package.optional-dependencies] +geoip = [ + { name = "geoip2" }, +] + [[package]] name = "certifi" version = "2025.7.9" @@ -491,13 +544,13 @@ dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "humanize" }, { name = "lark" }, - { name = "litellm" }, { name = "lxml" }, { name = "nltk" }, { name = "numpy" }, { name = "patchright" }, { name = "pillow" }, { name = "playwright" }, + { name = "playwright-stealth" }, { name = "psutil" }, { name = "pydantic" }, { name = "pyopenssl" }, @@ -508,12 +561,13 @@ dependencies = [ { name = "rich" }, { name = "shapely" }, { name = "snowballstemmer" }, - { name = "tf-playwright-stealth" }, + { name = "unclecode-litellm" }, { name = "xxhash" }, ] [package.optional-dependencies] all = [ + { name = "camoufox", extra = ["geoip"] }, { name = "nltk" }, { name = "pypdf" }, { name = "scikit-learn" }, @@ -523,6 +577,9 @@ all = [ { name = "torch" }, { name = "transformers" }, ] +camoufox = [ + { name = "camoufox", extra = ["geoip"] }, +] cosine = [ { name = "nltk" }, { name = "sentence-transformers" }, @@ -560,6 +617,8 @@ requires-dist = [ { name = "anyio", specifier = ">=4.0.0" }, { name = "beautifulsoup4", specifier = "~=4.12" }, { name = "brotli", specifier = ">=1.1.0" }, + { name = "camoufox", extras = ["geoip"], marker = "extra == 'all'", specifier = ">=0.4.0" }, + { name = "camoufox", extras = ["geoip"], marker = "extra == 'camoufox'", specifier = ">=0.4.0" }, { name = "chardet", specifier = ">=5.2.0" }, { name = "click", specifier = ">=8.1.7" }, { name = "cssselect", specifier = ">=1.2.0" }, @@ -568,7 +627,6 @@ requires-dist = [ { name = "httpx", extras = ["http2"], specifier = ">=0.27.2" }, { name = "humanize", specifier = ">=4.10.0" }, { name = "lark", specifier = ">=1.2.2" }, - { name = "litellm", specifier = ">=1.53.1" }, { name = "lxml", specifier = "~=5.3" }, { name = "nltk", specifier = ">=3.9.1" }, { name = "nltk", marker = "extra == 'all'" }, @@ -578,6 +636,7 @@ requires-dist = [ { name = "patchright", specifier = ">=1.49.0" }, { name = "pillow", specifier = ">=10.4" }, { name = "playwright", specifier = ">=1.49.0" }, + { name = "playwright-stealth", specifier = ">=2.0.0" }, { name = "psutil", specifier = ">=6.1.1" }, { name = "pydantic", specifier = ">=2.10" }, { name = "pyopenssl", specifier = ">=25.3.0" }, @@ -597,7 +656,6 @@ requires-dist = [ { name = "sentence-transformers", marker = "extra == 'transformer'" }, { name = "shapely", specifier = ">=2.0.0" }, { name = "snowballstemmer", specifier = "~=2.2" }, - { name = "tf-playwright-stealth", specifier = ">=1.1.0" }, { name = "tokenizers", marker = "extra == 'all'" }, { name = "tokenizers", marker = "extra == 'transformer'" }, { name = "torch", marker = "extra == 'all'" }, @@ -606,9 +664,10 @@ requires-dist = [ { name = "transformers", marker = "extra == 'all'" }, { name = "transformers", marker = "extra == 'cosine'" }, { name = "transformers", marker = "extra == 'transformer'" }, + { name = "unclecode-litellm", specifier = "==1.81.13" }, { name = "xxhash", specifier = "~=3.4" }, ] -provides-extras = ["pdf", "torch", "transformer", "cosine", "sync", "all"] +provides-extras = ["pdf", "torch", "transformer", "cosine", "camoufox", "sync", "all"] [package.metadata.requires-dev] dev = [{ name = "crawl4ai", editable = "." }] @@ -687,6 +746,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, ] +[[package]] +name = "cython" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/10/720e0fb84eab4c927c4dd6b61eb7993f7732dd83d29ba6d73083874eade9/cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e", size = 2960836, upload-time = "2026-01-04T14:14:51.103Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -701,21 +775,13 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "fake-http-header" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/0b/2849c87d9f13766e29c0a2f4d31681aa72e035016b251ab19d99bde7b592/fake_http_header-0.3.5-py3-none-any.whl", hash = "sha256:cd05f4bebf1b7e38b5f5c03d7fb820c0c17e87d9614fbee0afa39c32c7a2ad3c", size = 14938, upload-time = "2024-10-15T07:27:10.671Z" }, -] - [[package]] name = "fake-useragent" version = "2.2.0" @@ -725,6 +791,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" }, ] +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b2/731a6696e37cd20eed353f69a09f37a984a43c9713764ee3f7ad5f57f7f9/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", size = 516760, upload-time = "2025-10-19T22:25:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/c5/79/c73c47be2a3b8734d16e628982653517f80bbe0570e27185d91af6096507/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", size = 264748, upload-time = "2025-10-19T22:41:52.873Z" }, + { url = "https://files.pythonhosted.org/packages/24/c5/84c1eea05977c8ba5173555b0133e3558dc628bcf868d6bf1689ff14aedc/fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", size = 254537, upload-time = "2025-10-19T22:33:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/4e362367b7fa17dbed646922f216b9921efb486e7abe02147e4b917359f8/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d", size = 278994, upload-time = "2025-10-19T22:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/3985be633b5a428e9eaec4287ed4b873b7c4c53a9639a8b416637223c4cd/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", size = 280003, upload-time = "2025-10-19T22:23:45.415Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/6ef192a6df34e2266d5c9deb39cd3eea986df650cbcfeaf171aa52a059c3/fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", size = 303583, upload-time = "2025-10-19T22:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/9d/11/8a2ea753c68d4fece29d5d7c6f3f903948cc6e82d1823bc9f7f7c0355db3/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", size = 460955, upload-time = "2025-10-19T22:36:25.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/42/7a32c93b6ce12642d9a152ee4753a078f372c9ebb893bc489d838dd4afd5/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", size = 480763, upload-time = "2025-10-19T22:24:28.451Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e9/a5f6f686b46e3ed4ed3b93770111c233baac87dd6586a411b4988018ef1d/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", size = 452613, upload-time = "2025-10-19T22:25:06.827Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c9/18abc73c9c5b7fc0e476c1733b678783b2e8a35b0be9babd423571d44e98/fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a", size = 155045, upload-time = "2025-10-19T22:28:32.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8a/d9e33f4eb4d4f6d9f2c5c7d7e96b5cdbb535c93f3b1ad6acce97ee9d4bf8/fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4", size = 156122, upload-time = "2025-10-19T22:23:15.59Z" }, + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + [[package]] name = "filelock" version = "3.18.0" @@ -837,6 +966,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, ] +[[package]] +name = "geoip2" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "maxminddb" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/70/3d9e87289f79713aaf0fea9df4aa8e68776640fe59beb6299bb214610cfd/geoip2-5.2.0.tar.gz", hash = "sha256:6c9ded1953f8eb16043ed0a8ea20e6e9524ea7b65eb745724e12490aca44ef00", size = 176498, upload-time = "2025-11-20T18:21:08.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/d2/d55df737199a52b9d06e742ed2a608c525f0677e40375951372e65714fbd/geoip2-5.2.0-py3-none-any.whl", hash = "sha256:3d1546fd4eb7cad20445d027d2d9e81d3a71c074e019383f30db5d45e2c23320", size = 28991, upload-time = "2025-11-20T18:21:07.178Z" }, +] + [[package]] name = "greenlet" version = "3.2.3" @@ -1146,32 +1289,22 @@ wheels = [ ] [[package]] -name = "lark" -version = "1.2.2" +name = "language-tags" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/7e/b6a0efe4fee11e9742c1baaedf7c574084238a70b03c1d8eb2761383848f/language_tags-1.2.0.tar.gz", hash = "sha256:e934acba3e3dc85f867703eca421847a9ab7b7679b11b5d5cfd096febbf8bde6", size = 207901, upload-time = "2023-01-11T18:38:07.893Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/327554649ed2dd5ce59d3f5da176c7be20f9352c7c6c51597293660b7b08/language_tags-1.2.0-py3-none-any.whl", hash = "sha256:d815604622242fdfbbfd747b40c31213617fd03734a267f2e39ee4bd73c88722", size = 213449, upload-time = "2023-01-11T18:38:05.692Z" }, ] [[package]] -name = "litellm" -version = "1.74.0.post1" +name = "lark" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/68/a58b31f25c42fe052d7e7a0fc27d96d20c88b171070acc7b5d3cd553db0e/litellm-1.74.0.post1.tar.gz", hash = "sha256:417b08d0584ffc2788386261f1b3dea6e0b1b09b0231f48c46426c0c7a9b7278", size = 9032360, upload-time = "2025-07-07T17:48:08.376Z" } [[package]] name = "lxml" @@ -1325,6 +1458,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] +[[package]] +name = "maxminddb" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/83/bcd7f2e7dfcf601258a4eab92155816218e8f8adf6608d5f7d39da7ba863/maxminddb-3.1.1.tar.gz", hash = "sha256:b19a938c481518f19a2c534ffdcb3bc59582f0fbbdcf9f81ac9adf912a0af686", size = 212410, upload-time = "2026-03-05T18:14:19.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/7d/c0c4d69696be9b10e91ac9241f02339c465c35d05c54eab897e74e108f7c/maxminddb-3.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:44f5db2a50503dd805c6319e9dd2596b042382350ad570726ab6cab1e4404dd1", size = 53876, upload-time = "2026-03-05T18:12:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/f1/566fcd480a50ec199065cc823ffd4f6b96f8e4a997577e6e2f60004e2b73/maxminddb-3.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:60371de02e82177bb5507eae2761acd0f7b1b68f272920c04c1bf7d405e575fc", size = 36124, upload-time = "2026-03-05T18:12:46.126Z" }, + { url = "https://files.pythonhosted.org/packages/cd/41/b3bfcb489584d8f10d2364244adcfad1a91593132e61c5680d68b0ab85bc/maxminddb-3.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b7eb38ccb2323ddc06917dacaf67ad41b41a474911a473ad0856cbb75c0cb40", size = 35914, upload-time = "2026-03-05T18:12:47.354Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/8d3829aeb24497dc6fe29a7fbaf33d5bc64f3c1ad51663dcede3e7d51ff3/maxminddb-3.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cb1b1580d733e0ed8b1bbb10aef7a3f8c7d974b5affbf8f5af39060622562b", size = 101391, upload-time = "2026-03-05T18:12:48.745Z" }, + { url = "https://files.pythonhosted.org/packages/91/2e/1455860b6e25c7db2cb6673041e03ae450a920e57b89f1aceb82d2f8bd88/maxminddb-3.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:784043069847fb9ae8759b36d5b564855250fb58fa922fbffd3141bafe90fde0", size = 98742, upload-time = "2026-03-05T18:12:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d5/c7846ea7287503bcb8a9654a64fe2781b373d423255d84895441875fec61/maxminddb-3.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:00fbbbf27738169d8111d14718e5407752d505b8ede2ee408f463db888813a4a", size = 98795, upload-time = "2026-03-05T18:12:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/aa/71/a32818df12570445f55226e5a1b528d687b3516721bc6c50a42e8bc10dd7/maxminddb-3.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab143ad799092f48b53ced7e80e50539bf3b1601f0f046a6dfec55dc3d035e6d", size = 96737, upload-time = "2026-03-05T18:12:52.827Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/995380825d0b93649d2a3e61a0bdb4be313580e86ffc39aee9dc48474776/maxminddb-3.1.1-cp310-cp310-win32.whl", hash = "sha256:0b4c0872932652e1033e62c2f550637aeb44b78cf3ac5b229cb33381e46a85a9", size = 35420, upload-time = "2026-03-05T18:12:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/7fe207f43efe633d18666220a3927c355e1156c3d35fab20030cb03ef77c/maxminddb-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:57c4be48dfe5e0c10be75ccef37f169cd1aa3648a3a94a8ed7b39c6e0f101eb8", size = 37173, upload-time = "2026-03-05T18:12:55.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/2ff124617be7df8a69c25fb2a0bae51ea811ec510ac40e08eff64973c23c/maxminddb-3.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:2bf03080523b717f6c91495ec635d7eb68ed3d67aefdb56e86e251e550b11a2f", size = 34234, upload-time = "2026-03-05T18:12:55.976Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0c/bcbdc2d382c836ca71080201f97cc7b37cac1e048f8b433d784dcc67067f/maxminddb-3.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:af2a0e1d34a23244790a0882eccd5798735d8b363f88d52fcdcdf25dc752cb7d", size = 53870, upload-time = "2026-03-05T18:12:56.946Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c5/90727816da6f5e91b318fae1d3ff37bf4eaaa4fa8171e01ec9115da51d8d/maxminddb-3.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f1e89df992ad4ca506d2dc0776041d8670d6aaf9eaa1d2e60d315cab1932474", size = 36125, upload-time = "2026-03-05T18:12:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/3f1c08b66898e0a4d8f234f2a6f3613fef6a9a55a8793613307e7ea1a312/maxminddb-3.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b75feefc605bcb478f36ac70865f0c37498bd4abc37b101debac72e9eed3835c", size = 35914, upload-time = "2026-03-05T18:13:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5714a3a13c7e5e7151cfc50eb50f16b72cf714d8b377df92ef69fa514625/maxminddb-3.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff15158257dcd46b3d022f7ccecbd34e90d113c3f1f5b7372b78cf4c513b9a68", size = 101632, upload-time = "2026-03-05T18:13:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/9d/dd/d282142d5c9d511a6c9d60de9374d9c2f452638163711c8724a8034c0077/maxminddb-3.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f841b34ab6cef6df8d98d5c6e58f331ee2c32292145463a26192738547d3935", size = 98942, upload-time = "2026-03-05T18:13:02.927Z" }, + { url = "https://files.pythonhosted.org/packages/3b/66/791389029880ba8d36c516b84a726a185c2728e853ca0e2846a099bb523b/maxminddb-3.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a956c1db412e15921e9fe5af3f103acecfcd4760b2a1755dad4ce7eb2f814534", size = 99025, upload-time = "2026-03-05T18:13:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/71969c9ace2c98fb92cd6f6cf2842bd9f72aea64eabc0d872482de830852/maxminddb-3.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:283ccc25fca0b7944436e1dafd5e942e3410d91e0cff8704d1d5c99534a08e32", size = 96895, upload-time = "2026-03-05T18:13:05.005Z" }, + { url = "https://files.pythonhosted.org/packages/28/80/2ac383e6c63652103f9e19c3a1c3042a254d30e050f2beef849c6d2c1c82/maxminddb-3.1.1-cp311-cp311-win32.whl", hash = "sha256:8f1c040ceda871bb2857994bee3c4a34f59739552407628ae38aff4d1585c799", size = 35416, upload-time = "2026-03-05T18:13:06.407Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6d/eae87fb2f06333ccac36fbefe56c3f446588b7eb2e79247632f40ff90ba1/maxminddb-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:023bed7cce27556dcac761be4b41afef168e7603664ac3bfde26f35c01c15393", size = 37173, upload-time = "2026-03-05T18:13:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/b9bce95f04fd8ca02f424aa8ca5a2b9e58a504c90d5a794a47927e656e70/maxminddb-3.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:856cb7b6e20ac02c50b5201eca2daaa1a7efeecc4e2e885c4e7aabc372ae3bff", size = 34229, upload-time = "2026-03-05T18:13:09.392Z" }, + { url = "https://files.pythonhosted.org/packages/e4/32/331c6c0ce56aacee7f71b9b7ef2438ae74b2d788cd56d4a58cd3be3e6bf8/maxminddb-3.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ae6489a1b7fa4ab9b6ac5979d1eec1eed7cb7ef2f73777ddbb8fb8b9bec094e3", size = 54257, upload-time = "2026-03-05T18:13:10.656Z" }, + { url = "https://files.pythonhosted.org/packages/69/6d/4fc324d46b764e870847fc50d7e3b0154dbf165d04d121653066069e3d2c/maxminddb-3.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8f41a51bce83b5bbe4dc31b080787b7d4d83d8efa98778eb6f81df3ad9e98734", size = 36314, upload-time = "2026-03-05T18:13:11.75Z" }, + { url = "https://files.pythonhosted.org/packages/22/d5/436054930ccd384f2f6e17aa7689207e9334abbaed63bb573d76dbe0ee8f/maxminddb-3.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9cd4c05d08c22796e83aa54c70feb64121b3eae7257af35fbaced9f5d8d2081", size = 36105, upload-time = "2026-03-05T18:13:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/40/a2/bbded436f06c38716163f87d7d92a62f7d305d2f9f7e2e4155f8749a9f1e/maxminddb-3.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c4a402180154393c9c2502c7704b10a32a065661cd84196bbe7ac56869c6a82", size = 103427, upload-time = "2026-03-05T18:13:13.981Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/8464f1d29007736cfe1e0aa2dd1f3a36f5d7020abb9f14269b614a3f3ae1/maxminddb-3.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14d8b40d8e9b288cee18b8d80a7ba2a28211ce07b9c0e6ce721c5e685e3bf23c", size = 101252, upload-time = "2026-03-05T18:13:15.064Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9d/d3c95b64c05e90091ddef22a7a1bcdabe998f36b4a9f4b814382f712d83f/maxminddb-3.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eb2644548114b22d5808972d6b2b77d4c62084966b9a6be3853cd173ff745d5", size = 100587, upload-time = "2026-03-05T18:13:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0a/7e078abe41896d771e2917bc390fcd13186cd9b1b4ef8b451019f1fc342a/maxminddb-3.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a529873e376ada254c68a54d3ad13c8265eeedc5c56bdfdf25f1044b7f4177b", size = 99126, upload-time = "2026-03-05T18:13:18.001Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/f07411f340b70374d1b76b710b059cfc9874e22f596df3f20d26ebbece5b/maxminddb-3.1.1-cp312-cp312-win32.whl", hash = "sha256:9d98641b111eecc047b560d927379dd044bb36c0e399ee794e865b75ee8ef27a", size = 35632, upload-time = "2026-03-05T18:13:20.322Z" }, + { url = "https://files.pythonhosted.org/packages/a3/cc/1e42136d8416bbcaa81cdfdb26ec75263f106c0c34bf357ca98eebc394d0/maxminddb-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1ad6d3790ca4b2936f3e4ea971ef8383480fa069ed8ea4e5e6345f049d0e9f7", size = 37332, upload-time = "2026-03-05T18:13:21.363Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ed/f3a6b030eef252d4eaa811c87ad3a1d44376de581b11be8246fda1fc3716/maxminddb-3.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:c295f90ce99ce434d6a8477bd94ba4869ca04fe617ac99cea7548f0f6a3e4cd8", size = 34231, upload-time = "2026-03-05T18:13:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/a26cfdb9feed1ec0e66d5adeb37ff1f6bbcf3bb6c26870ec6a4554cacb93/maxminddb-3.1.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:90df0298f6713711ecba893f16ca31c486014e6b1c86611660426e5537cbbe14", size = 37596, upload-time = "2026-03-05T18:13:23.611Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/0bf2b7f2443b71a1c90d9d0772cecd43af392b0d285b20ddae9de9bd57da/maxminddb-3.1.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:42c8dc32b09e5f7c8e2e3d354e5bf48d56bf6c12099dd02df1ceca3f13762d97", size = 38079, upload-time = "2026-03-05T18:13:24.542Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cd/a05c211da1c29eaa00f971e30338823872dccf7b6c4935fc71b8d12d6b34/maxminddb-3.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:13a6dba0e696e904773cd9c17a290dcab9ba4a26128811e087c46c2c2f700c13", size = 35280, upload-time = "2026-03-05T18:13:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/0f4b34d9d9c5b79712d9616b00f3a683489a0b80ac2b61cb492d7d432977/maxminddb-3.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4d72b373930d95ac00db20a49b43eaac707cc633247f29a4c9a24000187505e9", size = 35823, upload-time = "2026-03-05T18:13:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/69/af/509eb9eeb119019f7f86a9e5bfdc1064e9bca590a07808adf9d4152221a6/maxminddb-3.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:afe667a377121a5234778ed0affb9b26bf9d23a0c50551541cc9e38e1c1d1400", size = 54236, upload-time = "2026-03-05T18:13:27.748Z" }, + { url = "https://files.pythonhosted.org/packages/91/6e/09949370a413a7b5b791e8784f527bfa216539451c8c9b801ff06e61effe/maxminddb-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9a297da0042877a1eef457e238aa4df1707eb7e254aa96ecb1e17e935939a670", size = 36308, upload-time = "2026-03-05T18:13:28.773Z" }, + { url = "https://files.pythonhosted.org/packages/8a/01/0bff084d31b4441ec00e8ec84fa961efe5dffd3359d5318a557db8302a09/maxminddb-3.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5c31f5a1e388847b642d8e1b375abfb7b327d51cfd85e9c9f938a3258df7369", size = 36051, upload-time = "2026-03-05T18:13:30.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/31/136f4afed0202d4dbc114668068ba6ef99ab4d5cb3860e8bfc49208965a5/maxminddb-3.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7060e43d0788259b3a9bcc3d604360ebd7b17915300c97f8e254faeb27a70c34", size = 103470, upload-time = "2026-03-05T18:13:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/75/1a/2593692d543498959b2836028ff26c36439343a2122f31a71202072f4e62/maxminddb-3.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb195714caeb419b9a33b07b0b721d620c770210dd955018453fc588a4b7e42", size = 101290, upload-time = "2026-03-05T18:13:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/d8/48/883b8961045ea624ac26ed04896bb7e0ccf911488695508bc20bf5cbe1ab/maxminddb-3.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b42f18cf17dfb3ff52ef8c97c41508d45cf23293d04779dce0fe2ca6f146e78", size = 100617, upload-time = "2026-03-05T18:13:33.74Z" }, + { url = "https://files.pythonhosted.org/packages/58/0a/bc52b27699601c235ccb973e5fbb0426520c8780f0404dd98e4d09b1ff88/maxminddb-3.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfcc3b074d4cfef4d15474368da70b47707df273cd89e1d6a92549f644852f5c", size = 99145, upload-time = "2026-03-05T18:13:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/95/2c/01a3c975622add0f6f04336b400fcf4270c7054c0a35d8622b6bbd4e091b/maxminddb-3.1.1-cp313-cp313-win32.whl", hash = "sha256:3c9d50b5964c00e998ba5fdbad95b62abdf0ed9da5a55eab173f89e38a285e47", size = 35626, upload-time = "2026-03-05T18:13:36.429Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/a64da9ae7dea91d24a12a5649bd62a0eda49ad5ecf184075947971e523ad/maxminddb-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:19be24c36219779e65be57897b36fea340223cafdf3b128f3249e8603be7744d", size = 37330, upload-time = "2026-03-05T18:13:37.355Z" }, + { url = "https://files.pythonhosted.org/packages/52/f8/0523374a421b9816da20de42256c7c11e43ce0663decb8b675cf3c0f4561/maxminddb-3.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:6d01a791db367768aa2e15b9c2df5bb4a8d8c11713d872dec5f33dd3e818af26", size = 34222, upload-time = "2026-03-05T18:13:38.355Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/b5069070975602ad14e7898b883dda0b0785dab3c24f5750f5b7fa4e14c3/maxminddb-3.1.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:b2f859ff9ab56b8ce1c2033fdd978273d432ef1b03fbba24dd28d284bc9e2b41", size = 37401, upload-time = "2026-03-05T18:13:39.624Z" }, + { url = "https://files.pythonhosted.org/packages/85/b3/1816167dbf9e1373f32548d26c45a74215750680e6a61943355e0d2d157a/maxminddb-3.1.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:40116947ad235692dff2f1590269a844d8623036caf99310a0ea6833b04673ca", size = 37923, upload-time = "2026-03-05T18:13:41.153Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/6e3a1ea6586fa49f2c438dd48ef9f7e67352729583f9ac6552f0d7d110ad/maxminddb-3.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b83155134842f6dcd06f9ba9b661028a2154debc41bfc6b8d665d67267891153", size = 35248, upload-time = "2026-03-05T18:13:42.122Z" }, + { url = "https://files.pythonhosted.org/packages/75/3c/bab279d11b8225e283175be8f6250366d362f131e282723946052c09cc2a/maxminddb-3.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:c2aa82dbb882714071403fe19b301a87a750eb8b58af60fd794afebe9447c0fb", size = 35798, upload-time = "2026-03-05T18:13:43.482Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e2/95baf6f117f9bc35de7c83b061b3fd27a49d03e0cda6c4dc5432167ac06e/maxminddb-3.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:65cbeaf809aaeb464e6c89086c45e0e4b9f15b73a28825fea0c570cf6fae18b6", size = 54226, upload-time = "2026-03-05T18:13:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c7/bdcf8ca67c7d93007a4ed512cec61a0e13b43cf9abbb9dfdacd458dff049/maxminddb-3.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ebb79b06f94577c37206a2143e157f4f8e6baf4f4473734b16993e04e0b1fdd7", size = 36368, upload-time = "2026-03-05T18:13:45.778Z" }, + { url = "https://files.pythonhosted.org/packages/da/71/a6cba811aa0ce539dff57400435643b2c05e607563f412c9138f50f14d34/maxminddb-3.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fac648527d83c4357f8f060f362a3c24f3aeb94bd458e2221522b7783dac5235", size = 36022, upload-time = "2026-03-05T18:13:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/c315b8e072663f77f928996fa6b8a084660d4130e00f092943fa9c892016/maxminddb-3.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c57d402cffae339e07224d04315ec1fd923a113dce5a9b2bf5894df8bdcfa", size = 103242, upload-time = "2026-03-05T18:13:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/d3/13/7914f150c633e8acbbcde6e37b58a280f79888d5668ae5a50bd3846fde2b/maxminddb-3.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bf4df891b71bb30ef0583effb0e694f610649ace69212def73dd20cc4ae8038", size = 101080, upload-time = "2026-03-05T18:13:49.127Z" }, + { url = "https://files.pythonhosted.org/packages/92/a5/d856e34333b80594443a82a384eb383a64c06dd7047a81efb42d129a6412/maxminddb-3.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5fa302b97d205d32e950bb38907a253a76d8a0091f2db5921e6561dbfa8febd1", size = 100611, upload-time = "2026-03-05T18:13:50.667Z" }, + { url = "https://files.pythonhosted.org/packages/19/56/bdc5bb7cdac55895aa69b76c4daa39c72c0bb9840439f08ba21e5b9f24c8/maxminddb-3.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3fa33c7e220106a3b9b24e5046c9573079730b62cad61b70897768f319d84323", size = 98959, upload-time = "2026-03-05T18:13:52.088Z" }, + { url = "https://files.pythonhosted.org/packages/26/ff/ef98fea99940ef8fc9e55607fcf1afbf37774a6e01815837f735427b29d7/maxminddb-3.1.1-cp314-cp314-win32.whl", hash = "sha256:16fa02b016f8d12e9d78a610a3abfe98510c7db2592cfebaaeb768067c10448f", size = 36291, upload-time = "2026-03-05T18:13:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/36/f8/2866267b7728d6877930a22de42e771010f94e5832432a71c1bb0ce1c2e3/maxminddb-3.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:0fa73771e95a1fc4a2c5f3530191473da9eb39ec8301999bd18d9ac6a9becb79", size = 38050, upload-time = "2026-03-05T18:13:54.072Z" }, + { url = "https://files.pythonhosted.org/packages/75/82/5b77a81e7ba8c8c83df1daa6cb701490589f5a17238a716d180a105ddf6d/maxminddb-3.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ad144247e94aca2e6f51408ce24ef9184f6bf440affce61090578382cdf69ffc", size = 34785, upload-time = "2026-03-05T18:13:56.232Z" }, + { url = "https://files.pythonhosted.org/packages/03/ef/72d6ef4f7acc428b8a8f3910f1acaacc33fcd55cd339029c33a234b6211f/maxminddb-3.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5334cee936368d43f7d99028bb37c864e8862465c6eac838e145d995e30707ec", size = 58136, upload-time = "2026-03-05T18:13:57.19Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a7/9f925bc30b77107b32cad2cab23aa5c459544079de62d6bcd3558dfe9a90/maxminddb-3.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff7d739fbefe2529b76dce0362a165795e369ca2d79e04882b42035ab2c16e44", size = 38443, upload-time = "2026-03-05T18:13:58.229Z" }, + { url = "https://files.pythonhosted.org/packages/86/8d/80c53840f598d77c01b94c718cc9b66658d677cf09b41f84d92aa04d0f81/maxminddb-3.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5d0709dae06d6f242acc1dd02495b6e1668ea898e22431865f8afc95aa4e7d0", size = 37937, upload-time = "2026-03-05T18:13:59.495Z" }, + { url = "https://files.pythonhosted.org/packages/4f/25/6a6f9aaf1af7c8ba83ab84792a7999a442b260bdadd84687800fde56fc4a/maxminddb-3.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e61dca3fd6709817762940f25fc86279957883a00c409612893a168974aba9f1", size = 120046, upload-time = "2026-03-05T18:14:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/e9/30/789efe80b43611cafa21bc130ae21c1b09ce7582c52a71d0f393ec69e868/maxminddb-3.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfb73a59ab7a3ccce1b00f048fdab982303253da26324943c831dd5d0f6db99b", size = 116454, upload-time = "2026-03-05T18:14:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/81/e2/da72e8a106539b40777ea0c991a1f5b896fe1374fce5d6a0d16977d49a1a/maxminddb-3.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7e7a0accc4011fd0528c9755010c9d4be06ee116cc0c2aff0ce0f63f792cb41", size = 116395, upload-time = "2026-03-05T18:14:02.689Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6d/e18a61f2c2ab08d92801525468be807c5e2f1fed5b817cd35d1c535669db/maxminddb-3.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3f5032043db990f159b0e8e2747468f4665a3b5b345e343f620fe71c91fb2631", size = 113548, upload-time = "2026-03-05T18:14:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/04/ba/015d8608e8ab108b6c5e8c252f51155385a70224f439e898ae01cd3aeb67/maxminddb-3.1.1-cp314-cp314t-win32.whl", hash = "sha256:962edb5f23f9e8dfbdfc775d9e452e4017adae2fb12d1b0a955dbddb7747e781", size = 37499, upload-time = "2026-03-05T18:14:04.823Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/8da91c3a89530209057e27f314859e17f8d93244b73945d01036e89cf819/maxminddb-3.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6879a4d822b894d1717aeef20b3558fdf1d1f6cf1ce25a7ad46d0a43ed745f63", size = 39557, upload-time = "2026-03-05T18:14:06.334Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3e/36c6010a302f822d054fed7ed7009d39dd0ed662c17a01e36dce956787ca/maxminddb-3.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9cc4eb1d2648c0188d7649babe8df1236d22972a753676db3b7487646a65b0c7", size = 35396, upload-time = "2026-03-05T18:14:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/96/9c/058db5f92ae21c8c897d083f06d4a58e7278218ef6c8faae04f0067435e5/maxminddb-3.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7dc138d3c113c1eb3bd8ad0d5ea6084166963fc2ddbb02ada31708c255e4d532", size = 35004, upload-time = "2026-03-05T18:14:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/33/1f/0cc52a8d514abcfc8ba0ac72c1d5912128837870bfc66f1fdb72f61a3a47/maxminddb-3.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d10c076dcae3b695ce29c6ae9ffc1ce6e39de4a147cabd25327a1421efe67977", size = 34651, upload-time = "2026-03-05T18:14:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/67/1f/78ecbd18288448ea7806770d53f6ad0ef27dc85f0825014dd02695e7dc9a/maxminddb-3.1.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7009b8997a7cb2a646cb425baa3d98e63bc5ef66ee01398386fe05c829eb3214", size = 39122, upload-time = "2026-03-05T18:14:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a7/c4065edf0ab6e4658e678bd0c6a777b0505f551f18174550cfc31606c931/maxminddb-3.1.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c67a57eb94b5d30cd7d65b4ce364dad5cb4871565bcd2afdc4a9908934485f4f", size = 37924, upload-time = "2026-03-05T18:14:11.241Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/a6cf1809ca0f9e77eb49671c24357d07ebd3b74b034bcbc289ea05c53c62/maxminddb-3.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7857b476784fe2c21e5de9a10022d7bb7d0681550388d2208fd88121ac709030", size = 37240, upload-time = "2026-03-05T18:14:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/65/6d/33c942a04e4f3cc95da23d566f0dc42146295a16ac053cf23969906fe809/maxminddb-3.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca0ba4b62cdcf285e9cfb829bfd6c2bfd620e0735075a9aee4ca71ddb4c293e2", size = 34944, upload-time = "2026-03-05T18:14:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/dd/73/8268d4082e774b9a7ca18e7ea6e0f159b0e7cbe6424d1a5d4eaea332fe7f/maxminddb-3.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:527afd09ec087711009ad8f28add76747d9b2ecb9f2bd5e9f1ca43ed18f3003e", size = 34594, upload-time = "2026-03-05T18:14:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/59/28/0fcc3be28859979b96126fcbd3bb0a6500eca21b838263139904085a1d6b/maxminddb-3.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1251ae02ba24a090411dbf8f4c5a4b580ccca68f43e83db1868ac0980087800c", size = 39123, upload-time = "2026-03-05T18:14:15.902Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/f4d7c4cb7c0111174d8730c4ad81e3068014aa4bb5930359a72501f3da2c/maxminddb-3.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3a368c70aed14a79a2ff6ff143428d952cb5eaa386862a97de15e52f283c4d1", size = 37925, upload-time = "2026-03-05T18:14:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/0d5d56226ac824141a2945eedc6ece70da4461e9e2a1edf340326396cb80/maxminddb-3.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2fcc510cb83e19029d35205e31fc7afea526bbbaad4764ca903c1dc18f62b2a", size = 37242, upload-time = "2026-03-05T18:14:18.539Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1642,7 +1861,7 @@ wheels = [ [[package]] name = "openai" -version = "1.93.3" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1654,9 +1873,90 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/66/fadc0cad6a229c6a85c3aa5f222a786ec4d9bf14c2a004f80ffa21dbaf21/openai-1.93.3.tar.gz", hash = "sha256:488b76399238c694af7e4e30c58170ea55e6f65038ab27dbe95b5077a00f8af8", size = 487595, upload-time = "2025-07-09T14:08:27.789Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/b9/0df6351b25c6bd494c534d2a8191dc9460fb5bb09c88b1427775d49fde05/openai-1.93.3-py3-none-any.whl", hash = "sha256:41aaa7594c7d141b46eed0a58dcd75d20edcc809fdd2c931ecbb4957dc98a892", size = 755132, upload-time = "2025-07-09T14:08:25.533Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/5d81f61fe3e4270da80c71442864c091cee3003cc8984c75f413fe742a07/orjson-3.11.8-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e6693ff90018600c72fd18d3d22fa438be26076cd3c823da5f63f7bab28c11cb", size = 229663, upload-time = "2026-03-31T16:14:30.708Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/85e06b0eb11de6fb424120fd5788a07035bd4c5e6bb7841ae9972a0526d1/orjson-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93de06bc920854552493c81f1f729fab7213b7db4b8195355db5fda02c7d1363", size = 132321, upload-time = "2026-03-31T16:14:32.317Z" }, + { url = "https://files.pythonhosted.org/packages/86/71/089338ee51b3132f050db0864a7df9bdd5e94c2a03820ab8a91e8f655618/orjson-3.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", size = 130658, upload-time = "2026-03-31T16:14:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/10/0d/f39d8802345d0ad65f7fd4374b29b9b59f98656dc30f21ca5c773265b2f0/orjson-3.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d823831105c01f6c8029faf297633dbeb30271892bd430e9c24ceae3734744", size = 135708, upload-time = "2026-03-31T16:14:35.224Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b5/40aae576b3473511696dcffea84fde638b2b64774eb4dcb8b2c262729f8a/orjson-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60c0423f15abb6cf78f56dff00168a1b582f7a1c23f114036e2bfc697814d5f", size = 147047, upload-time = "2026-03-31T16:14:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/778a84458d1fdaa634b2e572e51ce0b354232f580b2327e1f00a8d88c38c/orjson-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01928d0476b216ad2201823b0a74000440360cef4fed1912d297b8d84718f277", size = 133072, upload-time = "2026-03-31T16:14:37.715Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/1bbf2fc3ffcc4b829ade554b574af68cec898c9b5ad6420a923c75a073d3/orjson-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4a639049c44d36a6d1ae0f4a94b271605c745aee5647fa8ffaabcdc01b69a6", size = 133867, upload-time = "2026-03-31T16:14:39.356Z" }, + { url = "https://files.pythonhosted.org/packages/08/94/6413da22edc99a69a8d0c2e83bf42973b8aa94d83ef52a6d39ac85da00bc/orjson-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3222adff1e1ff0dce93c16146b93063a7793de6c43d52309ae321234cdaf0f4d", size = 142268, upload-time = "2026-03-31T16:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/aa5dbaa6136d7ba55f5461ac2e885efc6e6349424a428927fd46d68f4396/orjson-3.11.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3223665349bbfb68da234acd9846955b1a0808cbe5520ff634bf253a4407009b", size = 424008, upload-time = "2026-03-31T16:14:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/fa/aa/2c1962d108c7fe5e27aa03a354b378caf56d8eafdef15fd83dec081ce45a/orjson-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:61c9d357a59465736022d5d9ba06687afb7611dfb581a9d2129b77a6fcf78e59", size = 147942, upload-time = "2026-03-31T16:14:44.256Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/65f404f4c47eb1b0b4476f03ec838cac0c4aa933920ff81e5dda4dee14e7/orjson-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58fb9b17b4472c7b1dcf1a54583629e62e23779b2331052f09a9249edf81675b", size = 136640, upload-time = "2026-03-31T16:14:45.884Z" }, + { url = "https://files.pythonhosted.org/packages/90/5f/7b784aea98bdb125a2f2da7c27d6c2d2f6d943d96ef0278bae596d563f85/orjson-3.11.8-cp310-cp310-win32.whl", hash = "sha256:b43dc2a391981d36c42fa57747a49dae793ef1d2e43898b197925b5534abd10a", size = 132066, upload-time = "2026-03-31T16:14:47.397Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/2e284af8d6c9478df5ef938917743f61d68f4c70d17f1b6e82f7e3b8dba1/orjson-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:c98121237fea2f679480765abd566f7713185897f35c9e6c2add7e3a9900eb61", size = 127609, upload-time = "2026-03-31T16:14:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, ] [[package]] @@ -1758,6 +2058,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, ] +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + [[package]] name = "playwright" version = "1.53.0" @@ -1777,6 +2086,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/81/b42ff2116df5d07ccad2dc4eeb20af92c975a1fbc7cd3ed37b678468b813/playwright-1.53.0-py3-none-win_arm64.whl", hash = "sha256:fcfd481f76568d7b011571160e801b47034edd9e2383c43d83a5fb3f35c67885", size = 31188568, upload-time = "2025-06-25T21:49:00.194Z" }, ] +[[package]] +name = "playwright-stealth" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "playwright" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/db/6ade5d539c7d151b9defc78fafa8b65aa52352617d0e7699b47008bd801f/playwright_stealth-2.0.3.tar.gz", hash = "sha256:1d8e488fbdd8f190f1269ea8cf5d57d14df3a9f1af1001c41ee3588b2aac3133", size = 25751, upload-time = "2026-04-04T02:50:33.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl", hash = "sha256:1887ade423ab7ff8ae16d363a30a38de0b5817e1e4a29d47b74bf3a0e3dbfcb4", size = 34385, upload-time = "2026-04-04T02:50:35.246Z" }, +] + [[package]] name = "propcache" version = "0.3.2" @@ -2013,6 +2334,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/bf/3dbb1783388da54e650f8a6b88bde03c101d9ba93dfe8ab1b1873f1cd999/pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7", size = 676748, upload-time = "2025-11-14T09:30:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/aa/2b2d7ec3ac4b112a605e9bd5c5e5e4fd31d60a8a4b610ab19cc4838aa92a/pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48", size = 383825, upload-time = "2025-11-14T09:40:28.354Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + [[package]] name = "pyopenssl" version = "25.3.0" @@ -2456,6 +2810,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, ] +[[package]] +name = "screeninfo" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" }, +] + [[package]] name = "selenium" version = "4.34.2" @@ -2584,19 +2951,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -[[package]] -name = "tf-playwright-stealth" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fake-http-header" }, - { name = "playwright" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/6b/32bb58c65991f91aeaaf7473b650175d9d4af5dd383983d177d49ccba08d/tf_playwright_stealth-1.2.0.tar.gz", hash = "sha256:7bb8d32d3e60324fbf6b9eeae540b8cd9f3b9e07baeb33b025dbc98ad47658ba", size = 23362, upload-time = "2025-06-13T04:51:04.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/3d/2653f4cf49660bb44eeac8270617cc4c0287d61716f249f55053f0af0724/tf_playwright_stealth-1.2.0-py3-none-any.whl", hash = "sha256:26ee47ee89fa0f43c606fe37c188ea3ccd36f96ea90c01d167b768df457e7886", size = 33151, upload-time = "2025-06-13T04:51:03.769Z" }, -] - [[package]] name = "threadpoolctl" version = "3.6.0" @@ -2832,6 +3186,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] +[[package]] +name = "ua-parser" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ua-parser-builtins" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/0e/ed98be735bc89d5040e0c60f5620d0b8c04e9e7da99ed1459e8050e90a77/ua_parser-1.0.1.tar.gz", hash = "sha256:f9d92bf19d4329019cef91707aecc23c6d65143ad7e29a233f0580fb0d15547d", size = 728106, upload-time = "2025-02-01T14:13:32.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/37/be6dfbfa45719aa82c008fb4772cfe5c46db765a2ca4b6f524a1fdfee4d7/ua_parser-1.0.1-py3-none-any.whl", hash = "sha256:b059f2cb0935addea7e551251cbbf42e9a8872f86134163bc1a4f79e0945ffea", size = 31410, upload-time = "2025-02-01T14:13:28.458Z" }, +] + +[[package]] +name = "ua-parser-builtins" +version = "202603" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/6f/73a4d37deefb159556d39d654b5bad67b6874d1ad0b20b96fb5a04de3949/ua_parser_builtins-202603-py3-none-any.whl", hash = "sha256:67478397a68fac1a98fd0a31c416ea7c65a719141fc151d0211316f2cd337cc9", size = 89573, upload-time = "2026-03-01T20:50:02.491Z" }, +] + +[[package]] +name = "unclecode-litellm" +version = "1.81.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/c4/93ed52c49c2347184f908c692ebb7c1f06303805910774c3282ac68033db/unclecode_litellm-1.81.13.tar.gz", hash = "sha256:db70e34e3e859c0a07f02cb02eaa644f8fa4b4ecc5e2f3be9a58bd7d1c3feedc", size = 16678208, upload-time = "2026-03-24T14:46:31.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/85/7b1e0bc5827bcb23dc572b17c447fb5825340f36a9e4405d4b777b862e0c/unclecode_litellm-1.81.13-py3-none-any.whl", hash = "sha256:5e1fbedbed92333b48e7371e0bacf86d1288020451bf34351703c3b159591399", size = 18008619, upload-time = "2026-03-24T14:46:28.009Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"