From 310fb858e522c4d8ed73c1483c1b85795cbf6c05 Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Tue, 16 Dec 2025 09:54:20 -0800 Subject: [PATCH 01/16] remove auth ini logic --- development/CONTRIBUTING.md | 15 +- docs/cli_reference/cli_overview.mdx | 2 +- eval_protocol/auth.py | 242 +---------------- eval_protocol/cli.py | 34 --- eval_protocol/cli_commands/upload.py | 4 +- eval_protocol/cli_commands/utils.py | 19 +- tests/test_auth.py | 383 +++------------------------ 7 files changed, 65 insertions(+), 634 deletions(-) diff --git a/development/CONTRIBUTING.md b/development/CONTRIBUTING.md index 3d421cc8..d715fb69 100644 --- a/development/CONTRIBUTING.md +++ b/development/CONTRIBUTING.md @@ -106,24 +106,15 @@ export FIREWORKS_API_BASE="https://dev.api.fireworks.ai" # If targeting dev API **B. Configuration File (Lower Priority)** -If environment variables are not set, Eval Protocol will attempt to read credentials from `~/.fireworks/auth.ini`. - -Create or ensure the file `~/.fireworks/auth.ini` exists with the following format: -```ini -[fireworks] -api_key = YOUR_FIREWORKS_API_KEY -account_id = YOUR_FIREWORKS_ACCOUNT_ID -``` -Replace with your actual development credentials if using this method. +Eval Protocol does not read `~/.fireworks/auth.ini` (or any firectl profiles). Use environment variables instead. **Credential Sourcing Order:** Eval Protocol prioritizes credentials as follows: -1. Environment Variables (`FIREWORKS_API_KEY`, `FIREWORKS_ACCOUNT_ID`) -2. `~/.fireworks/auth.ini` configuration file +1. Environment Variables (`FIREWORKS_API_KEY`, optional `FIREWORKS_ACCOUNT_ID`) **Purpose of Credentials:** * `FIREWORKS_API_KEY`: Authenticates your requests to the Fireworks AI service. -* `FIREWORKS_ACCOUNT_ID`: Identifies your account for operations like managing evaluators. It specifies *where* (under which account) an operation should occur. +* `FIREWORKS_ACCOUNT_ID`: Identifies your account for operations like managing evaluators. Typically this is derived automatically from `FIREWORKS_API_KEY` via the `verifyApiKey` endpoint. * `FIREWORKS_API_BASE`: Allows targeting different API environments (e.g., development, staging). **Other Environment Variables:** diff --git a/docs/cli_reference/cli_overview.mdx b/docs/cli_reference/cli_overview.mdx index 05f63717..e9303d2f 100644 --- a/docs/cli_reference/cli_overview.mdx +++ b/docs/cli_reference/cli_overview.mdx @@ -329,7 +329,7 @@ The CLI recognizes the following environment variables: - `FIREWORKS_API_KEY`: Your Fireworks API key (required for deployment operations) - `FIREWORKS_API_BASE`: Base URL for the Fireworks API (defaults to `https://api.fireworks.ai`) -- `FIREWORKS_ACCOUNT_ID`: Your Fireworks account ID (optional, can be configured in auth.ini) +- `FIREWORKS_ACCOUNT_ID`: Your Fireworks account ID (optional; typically derived automatically from `FIREWORKS_API_KEY`) - `MODEL_AGENT`: Default agent model to use (e.g., "openai/gpt-4o-mini") - `MODEL_SIM`: Default simulation model to use (e.g., "openai/gpt-3.5-turbo") diff --git a/eval_protocol/auth.py b/eval_protocol/auth.py index f1d6c922..27781468 100644 --- a/eval_protocol/auth.py +++ b/eval_protocol/auth.py @@ -1,222 +1,24 @@ -import configparser import logging import os -from pathlib import Path -from typing import Dict, Optional # Added Dict +from typing import Optional import requests logger = logging.getLogger(__name__) -# Default locations (used for tests and as fallback). Actual resolution is dynamic via _get_auth_ini_file(). -FIREWORKS_CONFIG_DIR = Path.home() / ".fireworks" -AUTH_INI_FILE = FIREWORKS_CONFIG_DIR / "auth.ini" - - -def _get_profile_base_dir() -> Path: - """ - Resolve the Fireworks configuration base directory following firectl behavior: - - Default: ~/.fireworks - - If FIREWORKS_PROFILE is set and non-empty: ~/.fireworks/profiles/ - """ - profile_name = os.environ.get("FIREWORKS_PROFILE", "").strip() - base_dir = Path.home() / ".fireworks" - if profile_name: - base_dir = base_dir / "profiles" / profile_name - return base_dir - - -def _get_auth_ini_file() -> Path: - """ - Determine the auth.ini file path. - Priority: - 1) FIREWORKS_AUTH_FILE env var when set - 2) ~/.fireworks[/profiles/]/auth.ini (profile driven) - """ - auth_file_env = os.environ.get("FIREWORKS_AUTH_FILE") - if auth_file_env: - return Path(auth_file_env) - return _get_profile_base_dir() / "auth.ini" - - -def _is_profile_active() -> bool: - """ - Returns True if a specific profile or explicit auth file is active. - In this case, profile-based credentials should take precedence over env vars. - """ - if os.environ.get("FIREWORKS_AUTH_FILE"): - return True - prof = os.environ.get("FIREWORKS_PROFILE", "").strip() - return bool(prof) - - -def _parse_simple_auth_file(file_path: Path) -> Dict[str, str]: - """ - Parses an auth file with simple key=value lines. - Handles comments starting with # or ;. - Strips whitespace and basic quotes from values. - """ - creds = {} - if not file_path.exists(): - return creds - try: - with open(file_path, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#") or line.startswith(";"): - continue - if "=" in line: - key, value = line.split("=", 1) - key = key.strip() - value = value.strip() - # Remove surrounding quotes if present - if value and ( - (value.startswith('"') and value.endswith('"')) - or (value.startswith("'") and value.endswith("'")) - ): - value = value[1:-1] - - if key in ["api_key", "account_id"] and value: - creds[key] = value - except Exception as e: - logger.warning("Error during simple parsing of %s: %s", str(file_path), e) - return creds - - -def _get_credential_from_config_file(key_name: str) -> Optional[str]: - """ - Helper to get a specific credential (api_key or account_id) from auth.ini. - Tries simple parsing first, then configparser. - """ - auth_ini_path = _get_auth_ini_file() - if not auth_ini_path.exists(): - return None - - # 1. Try simple key-value parsing first - simple_creds = _parse_simple_auth_file(auth_ini_path) - if key_name in simple_creds: - logger.debug("Using %s from simple key-value parsing of %s.", key_name, str(auth_ini_path)) - return simple_creds[key_name] - - # 2. Fallback to configparser if not found via simple parsing or if simple parsing failed - # This path will also generate the "no section headers" warning if applicable, - # but only if simple parsing didn't yield the key. - try: - config = configparser.ConfigParser() - config.read(auth_ini_path) - - # Try [fireworks] section - if "fireworks" in config and config.has_option("fireworks", key_name): - value_from_file = config.get("fireworks", key_name) - if value_from_file: - logger.debug("Using %s from [fireworks] section in %s.", key_name, str(auth_ini_path)) - return value_from_file - - # Try default section (configparser might place items without section header here) - if config.has_option(config.default_section, key_name): - value_from_default = config.get(config.default_section, key_name) - if value_from_default: - logger.debug( - "Using %s from default section [%s] in %s.", - key_name, - config.default_section, - str(auth_ini_path), - ) - return value_from_default - - except configparser.MissingSectionHeaderError: - # This error implies the file is purely key-value, which simple parsing should have handled. - # If simple parsing failed to get the key, then it's likely not there or malformed. - logger.debug("%s has no section headers, and simple parsing did not find %s.", str(auth_ini_path), key_name) - except configparser.Error as e_config: - logger.warning("Configparser error reading %s for %s: %s", str(auth_ini_path), key_name, e_config) - except Exception as e_general: - logger.warning("Unexpected error reading %s for %s: %s", str(auth_ini_path), key_name, e_general) - - return None - - -def _get_credentials_from_config_file() -> Dict[str, Optional[str]]: - """ - Retrieve both api_key and account_id from auth.ini with a single read/parse. - Tries simple parsing first for both keys, then falls back to configparser for any missing ones. - Returns a dict with up to two keys: 'api_key' and 'account_id'. - """ - results: Dict[str, Optional[str]] = {} - auth_ini_path = _get_auth_ini_file() - if not auth_ini_path.exists(): - return results - - # 1) Simple key=value parsing - try: - simple_creds = _parse_simple_auth_file(auth_ini_path) - if "api_key" in simple_creds and simple_creds["api_key"]: - results["api_key"] = simple_creds["api_key"] - if "account_id" in simple_creds and simple_creds["account_id"]: - results["account_id"] = simple_creds["account_id"] - if "api_key" in results and "account_id" in results: - return results - except Exception as e: - logger.warning("Error during simple parsing of %s: %s", str(auth_ini_path), e) - - # 2) ConfigParser for any missing keys - try: - config = configparser.ConfigParser() - config.read(auth_ini_path) - for key_name in ("api_key", "account_id"): - if key_name in results and results[key_name]: - continue - if "fireworks" in config and config.has_option("fireworks", key_name): - value_from_file = config.get("fireworks", key_name) - if value_from_file: - results[key_name] = value_from_file - continue - if config.has_option(config.default_section, key_name): - value_from_default = config.get(config.default_section, key_name) - if value_from_default: - results[key_name] = value_from_default - except configparser.MissingSectionHeaderError: - # Purely key=value file without section headers; simple parsing should have handled it already. - logger.debug("%s has no section headers; falling back to simple parsing results.", str(auth_ini_path)) - except configparser.Error as e_config: - logger.warning("Configparser error reading %s: %s", str(auth_ini_path), e_config) - except Exception as e_general: - logger.warning("Unexpected error reading %s: %s", str(auth_ini_path), e_general) - - return results - def get_fireworks_api_key() -> Optional[str]: """ Retrieves the Fireworks API key. - The key is sourced in the following order: - 1. FIREWORKS_API_KEY environment variable. - 2. 'api_key' from the [fireworks] section of ~/.fireworks/auth.ini. - Returns: The API key if found, otherwise None. """ - # If a profile is active, prefer profile file first, then env - if _is_profile_active(): - api_key_from_file = _get_credential_from_config_file("api_key") - if api_key_from_file: - return api_key_from_file - api_key = os.environ.get("FIREWORKS_API_KEY") - if api_key: - logger.debug("Using FIREWORKS_API_KEY from environment variable (profile active but file missing).") - return api_key - else: - # Default behavior: env overrides file - api_key = os.environ.get("FIREWORKS_API_KEY") - if api_key: - logger.debug("Using FIREWORKS_API_KEY from environment variable.") - return api_key - api_key_from_file = _get_credential_from_config_file("api_key") - if api_key_from_file: - return api_key_from_file - - logger.debug("Fireworks API key not found in environment variables or auth.ini.") + api_key = os.environ.get("FIREWORKS_API_KEY") + if api_key and api_key.strip(): + logger.debug("Using FIREWORKS_API_KEY from environment variable.") + return api_key.strip() + logger.debug("Fireworks API key not found in environment variables.") return None @@ -226,36 +28,18 @@ def get_fireworks_account_id() -> Optional[str]: The Account ID is sourced in the following order: 1. FIREWORKS_ACCOUNT_ID environment variable. - 2. 'account_id' from the [fireworks] section of ~/.fireworks/auth.ini. - 3. If an API key is available (env or auth.ini), resolve via verifyApiKey. + 2. If an API key is available (env), resolve via verifyApiKey. Returns: The Account ID if found, otherwise None. """ - # If a profile is active, prefer profile file first, then env - if _is_profile_active(): - creds = _get_credentials_from_config_file() - account_id_from_file = creds.get("account_id") - if account_id_from_file: - return account_id_from_file - account_id = os.environ.get("FIREWORKS_ACCOUNT_ID") - if account_id: - logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable (profile active but file missing).") - return account_id - else: - # Default behavior: env overrides file - account_id = os.environ.get("FIREWORKS_ACCOUNT_ID") - if account_id: - logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable.") - return account_id - creds = _get_credentials_from_config_file() - account_id_from_file = creds.get("account_id") - if account_id_from_file: - return account_id_from_file + account_id = os.environ.get("FIREWORKS_ACCOUNT_ID") + if account_id and account_id.strip(): + logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable.") + return account_id.strip() - # 3) Fallback: if API key is present, attempt to resolve via verifyApiKey (env or auth.ini) + # Fallback: if API key is present, attempt to resolve via verifyApiKey (env) try: - # Intentionally use get_fireworks_api_key to centralize precedence (env vs file) api_key_for_verify = get_fireworks_api_key() if api_key_for_verify: resolved = verify_api_key_and_get_account_id(api_key=api_key_for_verify, api_base=get_fireworks_api_base()) @@ -265,7 +49,7 @@ def get_fireworks_account_id() -> Optional[str]: except Exception as e: logger.debug("Failed to resolve FIREWORKS_ACCOUNT_ID via verifyApiKey: %s", e) - logger.debug("Fireworks Account ID not found in environment variables, auth.ini, or via verifyApiKey.") + logger.debug("Fireworks Account ID not found in environment variables or via verifyApiKey.") return None diff --git a/eval_protocol/cli.py b/eval_protocol/cli.py index e8125390..8aa7fdb0 100644 --- a/eval_protocol/cli.py +++ b/eval_protocol/cli.py @@ -36,10 +36,6 @@ def parse_args(args=None): """Parse command line arguments""" parser = argparse.ArgumentParser(description="eval-protocol: Tools for evaluation and reward modeling") parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") - parser.add_argument( - "--profile", - help="Fireworks profile to use (reads ~/.fireworks/profiles//auth.ini and settings.ini)", - ) parser.add_argument( "--server", help="Fireworks API server hostname or URL (e.g., dev.api.fireworks.ai or https://dev.api.fireworks.ai)", @@ -537,38 +533,8 @@ def _extract_flag_value(argv_list, flag_name): return tok.split("=", 1)[1] return None - pre_profile = _extract_flag_value(raw_argv, "--profile") pre_server = _extract_flag_value(raw_argv, "--server") - # Handle Fireworks profile selection early so downstream modules see the env - profile = pre_profile - if profile: - try: - os.environ["FIREWORKS_PROFILE"] = profile - # Mirror firectl behavior: ~/.fireworks[/profiles/] - base_dir = Path.home() / ".fireworks" - if profile: - base_dir = base_dir / "profiles" / profile - os.makedirs(str(base_dir), mode=0o700, exist_ok=True) - - # Provide helpful env hints for consumers (optional) - os.environ["FIREWORKS_AUTH_FILE"] = str(base_dir / "auth.ini") - os.environ["FIREWORKS_SETTINGS_FILE"] = str(base_dir / "settings.ini") - logger.debug("Using Fireworks profile '%s' at %s", profile, base_dir) - except OSError as e: - logger.warning("Failed to initialize Fireworks profile '%s': %s", profile, e) - - # Proactively resolve and export account_id from the active profile to avoid stale .env overrides - try: - from eval_protocol.auth import get_fireworks_account_id as _resolve_account_id - - resolved_account = _resolve_account_id() - if resolved_account: - os.environ["FIREWORKS_ACCOUNT_ID"] = resolved_account - logger.debug("Resolved account_id from profile '%s': %s", profile, resolved_account) - except Exception as e: # noqa: B902 - logger.debug("Unable to resolve account_id from profile '%s': %s", profile, e) - # Handle Fireworks server selection early server = pre_server if server: diff --git a/eval_protocol/cli_commands/upload.py b/eval_protocol/cli_commands/upload.py index 33e0ed2f..c978d48c 100644 --- a/eval_protocol/cli_commands/upload.py +++ b/eval_protocol/cli_commands/upload.py @@ -248,7 +248,9 @@ def upload_command(args: argparse.Namespace) -> int: print(f"Warning: Failed to create/update {secret_name} secret on Fireworks.") else: if not fw_account_id: - print("Warning: FIREWORKS_ACCOUNT_ID not found; cannot register secrets.") + print( + "Warning: Could not resolve Fireworks account id from FIREWORKS_API_KEY; cannot register secrets." + ) if not secrets_from_file: print("Warning: No API keys found in environment or .env file; no secrets to register.") except Exception as e: diff --git a/eval_protocol/cli_commands/utils.py b/eval_protocol/cli_commands/utils.py index f6e2525f..d49b97e9 100644 --- a/eval_protocol/cli_commands/utils.py +++ b/eval_protocol/cli_commands/utils.py @@ -397,14 +397,19 @@ def _normalize_evaluator_id(evaluator_id: str) -> str: def _ensure_account_id() -> Optional[str]: """Resolve and cache FIREWORKS_ACCOUNT_ID if possible.""" - account_id = get_fireworks_account_id() + account_id = os.environ.get("FIREWORKS_ACCOUNT_ID") + if account_id and account_id.strip(): + return account_id.strip() + api_key = get_fireworks_api_key() - if not account_id and api_key: - resolved = verify_api_key_and_get_account_id(api_key=api_key, api_base=get_fireworks_api_base()) - if resolved: - os.environ["FIREWORKS_ACCOUNT_ID"] = resolved - return resolved - return account_id + if not api_key: + return None + + resolved = verify_api_key_and_get_account_id(api_key=api_key, api_base=get_fireworks_api_base()) + if resolved: + os.environ["FIREWORKS_ACCOUNT_ID"] = resolved + return resolved + return None def _extract_terminal_segment(resource_name: str) -> str: diff --git a/tests/test_auth.py b/tests/test_auth.py index 72dd54ca..ef280557 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,44 +1,31 @@ -import configparser # Import the original for type hinting if needed, but not for spec. import os - -# Import the original ConfigParser for use in spec if absolutely necessary, -# though direct configuration of the mock instance is preferred. -from configparser import ConfigParser as OriginalConfigParser -from pathlib import Path -from unittest.mock import MagicMock, mock_open, patch +from unittest.mock import patch import pytest -# Import the SUT from eval_protocol.auth import ( - AUTH_INI_FILE, get_fireworks_account_id, get_fireworks_api_key, + verify_api_key_and_get_account_id, ) -# Test data + TEST_ENV_API_KEY = "test_env_api_key_123" TEST_ENV_ACCOUNT_ID = "test_env_account_id_456" -INI_API_KEY = "ini_api_key_abc" -INI_ACCOUNT_ID = "ini_account_id_def" @pytest.fixture(autouse=True) def clear_env_vars_fixture(): - env_vars_to_clear = ["FIREWORKS_API_KEY", "FIREWORKS_ACCOUNT_ID"] + env_vars_to_clear = ["FIREWORKS_API_KEY", "FIREWORKS_ACCOUNT_ID", "FIREWORKS_API_BASE"] original_values = {var: os.environ.get(var) for var in env_vars_to_clear} for var in env_vars_to_clear: - if var in os.environ: - del os.environ[var] + os.environ.pop(var, None) yield for var, value in original_values.items(): - if value is not None: + if value is None: + os.environ.pop(var, None) + else: os.environ[var] = value - elif var in os.environ: - del os.environ[var] - - -# --- Tests for get_fireworks_api_key --- def test_get_api_key_from_env(): @@ -46,160 +33,8 @@ def test_get_api_key_from_env(): assert get_fireworks_api_key() == TEST_ENV_API_KEY -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") # Mocks the ConfigParser class -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) # Ensure simple parse finds nothing -def test_get_api_key_from_ini(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - # Configure the instance that configparser.ConfigParser() will return - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - - # Simulate key found in [fireworks] section - def has_option_fireworks_true(section, option): - if section == "fireworks": - return option == "api_key" - return False # Not in default or other sections for this test - - def get_fireworks_value(section, option): - if section == "fireworks" and option == "api_key": - return INI_API_KEY - raise configparser.NoOptionError(option, section) - - mock_parser_instance.has_option.side_effect = has_option_fireworks_true - mock_parser_instance.get.side_effect = get_fireworks_value - # Ensure 'fireworks' section itself exists - mock_parser_instance.__contains__.side_effect = lambda item: item == "fireworks" # For "fireworks" in config check - - with patch( - "builtins.open", mock_open(read_data="[fireworks]\napi_key = foo") - ): # Actual read_data not used by mock parser - assert get_fireworks_api_key() == INI_API_KEY - - mock_path_exists.assert_called_once_with() - mock_ConfigParser_class.assert_called_once_with() # Class was instantiated - mock_parser_instance.read.assert_called_once_with(AUTH_INI_FILE) - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -def test_get_api_key_env_overrides_ini(): - os.environ["FIREWORKS_API_KEY"] = TEST_ENV_API_KEY - with ( - patch("pathlib.Path.exists") as mock_path_exists, - patch("configparser.ConfigParser") as mock_ConfigParser_class, - patch("eval_protocol.auth._parse_simple_auth_file") as mock_parse_simple, - ): - assert get_fireworks_api_key() == TEST_ENV_API_KEY - mock_parse_simple.assert_not_called() # Env var should be checked first - mock_path_exists.assert_not_called() - mock_ConfigParser_class.assert_not_called() - - -@patch("pathlib.Path.exists", return_value=False) -def test_get_api_key_not_found(mock_path_exists): - # Ensure _parse_simple_auth_file is also considered if Path.exists is True but file is empty/no key - with patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) as mock_parse_simple: - assert get_fireworks_api_key() is None - # _get_credential_from_config_file checks AUTH_INI_FILE.exists() first - # if it's false, _parse_simple_auth_file won't be called by it. - # However, get_fireworks_api_key calls _get_credential_from_config_file, - # which itself calls _parse_simple_auth_file if AUTH_INI_FILE.exists(). - # This test specifically tests when AUTH_INI_FILE does *not* exist. - mock_parse_simple.assert_not_called() # Because AUTH_INI_FILE.exists() is False - mock_path_exists.assert_called_once_with() - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) # Simple parse finds nothing -def test_get_api_key_ini_exists_no_section(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - # Simulate MissingSectionHeaderError to trigger fallback within configparser logic (though simple parse is first) - mock_parser_instance.read.side_effect = configparser.MissingSectionHeaderError("file", 1, "line") - - with patch( - "builtins.open", # This mock_open is for the configparser's attempt if it were reached - mock_open(read_data="other_key = some_val_but_no_section_header\nanother=val"), - ): - assert get_fireworks_api_key() is None - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_api_key_ini_exists_no_key_option(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - - mock_parser_instance.__contains__.side_effect = lambda item: item == "fireworks" - mock_parser_instance.has_option.side_effect = lambda section, option: False - - with patch("builtins.open", mock_open(read_data="[fireworks]\nsome_other_key=foo")): - assert get_fireworks_api_key() is None - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_api_key_ini_empty_value(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - - mock_parser_instance.__contains__.side_effect = lambda item: item == "fireworks" - mock_parser_instance.has_option.side_effect = ( - lambda section, option: section == "fireworks" and option == "api_key" - ) - mock_parser_instance.get.side_effect = lambda section, option: ( - "" if section == "fireworks" and option == "api_key" else configparser.NoOptionError(option, section) - ) - - with patch("builtins.open", mock_open(read_data="[fireworks]\napi_key=")): - assert get_fireworks_api_key() is None - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_api_key_from_ini_default_section_success(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - - def has_option_logic(section, option): - if section == "fireworks": - return False - return section == mock_parser_instance.default_section and option == "api_key" - - def get_logic(section, option): - if section == mock_parser_instance.default_section and option == "api_key": - return INI_API_KEY - raise configparser.NoOptionError(option, section) - - mock_parser_instance.has_option.side_effect = has_option_logic - mock_parser_instance.get.side_effect = get_logic - mock_parser_instance.__contains__.side_effect = lambda item: item != "fireworks" - - with patch("builtins.open", mock_open(read_data="api_key = ini_api_key_abc")): - assert get_fireworks_api_key() == INI_API_KEY - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -@patch("pathlib.Path.exists", return_value=True) -# We don't mock ConfigParser here because we are testing the _parse_simple_auth_file path directly -def test_get_api_key_from_ini_simple_parsing_success(mock_path_exists): - file_content = f"api_key = {INI_API_KEY}\nother_key = value" - # Patch open specifically for _parse_simple_auth_file if it's different from configparser's use - with patch("eval_protocol.auth.open", mock_open(read_data=file_content), create=True): - # Ensure configparser path is not taken or also returns None for api_key - with patch("configparser.ConfigParser") as mock_ConfigParser_class_inner: - mock_parser_instance = mock_ConfigParser_class_inner.return_value - mock_parser_instance.read.return_value = [] # Simulate configparser finds nothing - mock_parser_instance.has_option.return_value = False - assert get_fireworks_api_key() == INI_API_KEY - - -# --- Tests for get_fireworks_account_id --- +def test_get_api_key_not_found(): + assert get_fireworks_api_key() is None def test_get_account_id_from_env(): @@ -207,190 +42,38 @@ def test_get_account_id_from_env(): assert get_fireworks_account_id() == TEST_ENV_ACCOUNT_ID -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) # Ensure simple parse finds nothing -def test_get_account_id_from_ini(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - - def has_option_fireworks_true(section, option): - if section == "fireworks": - return option == "account_id" - return False - - def get_fireworks_value(section, option): - if section == "fireworks" and option == "account_id": - return INI_ACCOUNT_ID - raise configparser.NoOptionError(option, section) - - mock_parser_instance.has_option.side_effect = has_option_fireworks_true - mock_parser_instance.get.side_effect = get_fireworks_value - mock_parser_instance.__contains__.side_effect = lambda item: item == "fireworks" - - with patch("builtins.open", mock_open(read_data="[fireworks]\naccount_id = foo")): - assert get_fireworks_account_id() == INI_ACCOUNT_ID - - mock_path_exists.assert_called_once_with() - mock_ConfigParser_class.assert_called_once_with() - mock_parser_instance.read.assert_called_once_with(AUTH_INI_FILE) - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -def test_get_account_id_env_overrides_ini(): - os.environ["FIREWORKS_ACCOUNT_ID"] = TEST_ENV_ACCOUNT_ID - with ( - patch("pathlib.Path.exists") as mock_path_exists, - patch("configparser.ConfigParser") as mock_ConfigParser_class, - patch("eval_protocol.auth._parse_simple_auth_file") as mock_parse_simple, - ): - assert get_fireworks_account_id() == TEST_ENV_ACCOUNT_ID - mock_parse_simple.assert_not_called() - mock_path_exists.assert_not_called() - mock_ConfigParser_class.assert_not_called() - - -@patch("pathlib.Path.exists", return_value=False) -def test_get_account_id_not_found(mock_path_exists): - with patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) as mock_parse_simple: - assert get_fireworks_account_id() is None - mock_parse_simple.assert_not_called() - # With verify fallback using get_fireworks_api_key, exists() may be checked more than once - assert mock_path_exists.call_count >= 1 - +def test_get_account_id_not_found(): + assert get_fireworks_account_id() is None -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_account_id_ini_exists_no_section(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.side_effect = configparser.MissingSectionHeaderError("file", 1, "line") - with patch( - "builtins.open", - mock_open(read_data="other_key = some_val_but_no_section_header\nanother=val"), - ): - assert get_fireworks_account_id() is None - # Fallback verify path may trigger a second simple parse for api_key; ensure at least one call - assert mock_parse_simple.call_count >= 1 - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_account_id_ini_exists_no_id_option(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - mock_parser_instance.__contains__.side_effect = lambda item: item == "fireworks" - mock_parser_instance.has_option.side_effect = lambda section, option: False - - with patch("builtins.open", mock_open(read_data="[fireworks]\nsome_other_key=foo")): - assert get_fireworks_account_id() is None - # Fallback verify path may trigger a second simple parse for api_key; ensure at least one call - assert mock_parse_simple.call_count >= 1 - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_account_id_ini_empty_value(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - mock_parser_instance.__contains__.side_effect = lambda item: item == "fireworks" - mock_parser_instance.has_option.side_effect = ( - lambda section, option: section == "fireworks" and option == "account_id" - ) - mock_parser_instance.get.side_effect = lambda section, option: ( - "" if section == "fireworks" and option == "account_id" else configparser.NoOptionError(option, section) - ) - with patch("builtins.open", mock_open(read_data="[fireworks]\naccount_id=")): - assert get_fireworks_account_id() is None - # Fallback verify path may trigger a second simple parse for api_key; ensure at least one call - assert mock_parse_simple.call_count >= 1 - - -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) -def test_get_account_id_from_ini_default_section_success(mock_parse_simple, mock_ConfigParser_class, mock_path_exists): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.return_value = [str(AUTH_INI_FILE)] - - def has_option_logic(section, option): - if section == "fireworks": - return False - return section == mock_parser_instance.default_section and option == "account_id" - - def get_logic(section, option): - if section == mock_parser_instance.default_section and option == "account_id": - return INI_ACCOUNT_ID - raise configparser.NoOptionError(option, section) - - mock_parser_instance.has_option.side_effect = has_option_logic - mock_parser_instance.get.side_effect = get_logic - mock_parser_instance.__contains__.side_effect = lambda item: item != "fireworks" - with patch("builtins.open", mock_open(read_data="account_id = ini_account_id_def")): - assert get_fireworks_account_id() == INI_ACCOUNT_ID - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) - - -@patch("pathlib.Path.exists", return_value=True) -# We don't mock ConfigParser here because we are testing the _parse_simple_auth_file path directly -def test_get_account_id_from_ini_simple_parsing_success( - mock_path_exists, -): # Renamed from fallback_parsing - file_content = f"account_id = {INI_ACCOUNT_ID}\nother_key = value" - with patch("eval_protocol.auth.open", mock_open(read_data=file_content), create=True): - # Ensure configparser path is not taken or also returns None for account_id - with patch("configparser.ConfigParser") as mock_ConfigParser_class_inner: - mock_parser_instance = mock_ConfigParser_class_inner.return_value - mock_parser_instance.read.return_value = [] - mock_parser_instance.has_option.return_value = False - assert get_fireworks_account_id() == INI_ACCOUNT_ID +def test_verify_api_key_and_get_account_id_success_from_header(): + os.environ["FIREWORKS_API_KEY"] = TEST_ENV_API_KEY -# --- Tests for error handling --- + class _Resp: + status_code = 200 + headers = {"x-fireworks-account-id": "acct_123"} + with patch("eval_protocol.auth.requests.get", return_value=_Resp()): + assert verify_api_key_and_get_account_id() == "acct_123" -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) # Simple parse finds nothing -def test_get_api_key_ini_parse_error(mock_parse_simple, mock_ConfigParser_class, mock_path_exists, caplog): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.side_effect = configparser.Error("Mocked Parsing Error") - with patch("builtins.open", mock_open(read_data="malformed ini content")): - assert get_fireworks_api_key() is None - assert "Configparser error reading" in caplog.text - assert "Mocked Parsing Error" in caplog.text - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) +def test_verify_api_key_and_get_account_id_non_200_returns_none(): + os.environ["FIREWORKS_API_KEY"] = TEST_ENV_API_KEY + class _Resp: + status_code = 403 + headers = {"x-fireworks-account-id": "acct_789"} -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) # Simple parse finds nothing -def test_get_account_id_ini_parse_error(mock_parse_simple, mock_ConfigParser_class, mock_path_exists, caplog): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.side_effect = configparser.Error("Mocked Parsing Error") + with patch("eval_protocol.auth.requests.get", return_value=_Resp()): + assert verify_api_key_and_get_account_id() is None - with patch("builtins.open", mock_open(read_data="malformed ini content")): - assert get_fireworks_account_id() is None - assert "Configparser error reading" in caplog.text - assert "Mocked Parsing Error" in caplog.text - # Fallback verify path may trigger a second simple parse for api_key; ensure at least one call - assert mock_parse_simple.call_count >= 1 +def test_get_account_id_resolves_via_verify_when_api_key_present(): + os.environ["FIREWORKS_API_KEY"] = TEST_ENV_API_KEY -@patch("pathlib.Path.exists", return_value=True) -@patch("configparser.ConfigParser") -@patch("eval_protocol.auth._parse_simple_auth_file", return_value={}) # Simple parse finds nothing -def test_get_api_key_unexpected_error_reading_ini( - mock_parse_simple, mock_ConfigParser_class, mock_path_exists, caplog -): - mock_parser_instance = mock_ConfigParser_class.return_value - mock_parser_instance.read.side_effect = Exception("Unexpected Read Error") + class _Resp: + status_code = 200 + headers = {"X-Fireworks-Account-Id": "acct_456"} - with patch("builtins.open", mock_open(read_data="ini content")): - assert get_fireworks_api_key() is None - assert "Unexpected error reading" in caplog.text # This comes from _get_credential_from_config_file - assert "Unexpected Read Error" in caplog.text - mock_parse_simple.assert_called_once_with(AUTH_INI_FILE) + with patch("eval_protocol.auth.requests.get", return_value=_Resp()): + assert get_fireworks_account_id() == "acct_456" From b38f54c7e897be6c1ea7c382d7a6aeb20b775903 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 16 Dec 2025 15:46:14 -0800 Subject: [PATCH 02/16] update fireworks-ai and make it a core dependency --- pyproject.toml | 4 +- uv.lock | 252 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 248 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 60ed1346..52bc8c1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "pytest-asyncio>=0.21.0", "peewee>=3.18.2", "backoff>=2.2.0", + "fireworks>=1.0.0a11", "questionary>=2.0.0", # Dependencies for vendored tau2 package "toml>=0.10.0", @@ -89,9 +90,6 @@ trl = [ openevals = [ "openevals>=0.1.0", ] -fireworks = [ - "fireworks-ai>=0.19.19", -] box2d = [ "swig", "gymnasium[box2d]>=0.29.0", diff --git a/uv.lock b/uv.lock index 38b07c4a..62cab335 100644 --- a/uv.lock +++ b/uv.lock @@ -457,6 +457,15 @@ css = [ { name = "tinycss2" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "boto3" version = "1.40.17" @@ -1221,6 +1230,7 @@ dependencies = [ { name = "deepdiff" }, { name = "docstring-parser" }, { name = "fastapi" }, + { name = "fireworks" }, { name = "httpx" }, { name = "hydra-core" }, { name = "litellm" }, @@ -1285,9 +1295,6 @@ dev = [ { name = "versioneer" }, { name = "werkzeug" }, ] -fireworks = [ - { name = "fireworks-ai" }, -] huggingface = [ { name = "datasets" }, { name = "transformers" }, @@ -1364,7 +1371,7 @@ requires-dist = [ { name = "docstring-parser", specifier = ">=0.15" }, { name = "e2b", marker = "extra == 'dev'" }, { name = "fastapi", specifier = ">=0.116.1" }, - { name = "fireworks-ai", marker = "extra == 'fireworks'", specifier = ">=0.19.19" }, + { name = "fireworks", specifier = ">=1.0.0a11" }, { name = "google-auth", marker = "extra == 'bigquery'", specifier = ">=2.0.0" }, { name = "google-cloud-bigquery", marker = "extra == 'bigquery'", specifier = ">=3.0.0" }, { name = "gymnasium", marker = "extra == 'dev'", specifier = ">=1.2.0" }, @@ -1434,7 +1441,7 @@ requires-dist = [ { name = "websockets", specifier = ">=15.0.1" }, { name = "werkzeug", marker = "extra == 'dev'", specifier = ">=2.0.0" }, ] -provides-extras = ["dev", "trl", "openevals", "fireworks", "box2d", "langfuse", "huggingface", "langsmith", "bigquery", "svgbench", "pydantic", "supabase", "chinook", "langchain", "braintrust", "openenv", "langgraph", "langgraph-tools", "proxy"] +provides-extras = ["dev", "trl", "openevals", "box2d", "langfuse", "huggingface", "langsmith", "bigquery", "svgbench", "pydantic", "supabase", "chinook", "langchain", "braintrust", "openenv", "langgraph", "langgraph-tools", "proxy"] [package.metadata.requires-dev] dev = [ @@ -1631,6 +1638,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] +[[package]] +name = "fireworks" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "flask-paginate" }, + { name = "gunicorn" }, + { name = "jinja2" }, + { name = "monty" }, + { name = "pymongo" }, + { name = "python-dateutil" }, + { name = "ruamel-yaml" }, + { name = "tabulate" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/18/d020950eb8566d5eceffbde8597cc31405d6bfc327088212e5d95cf8a556/fireworks-2.0.8.tar.gz", hash = "sha256:2c7cc91baa2eb3f3518d78e97a78bc46116885d72d465e4aeef9b0dca320187a", size = 5026459, upload-time = "2025-12-05T20:40:09.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/03/cf5fd9be0a54f632b93d49cb3da72242e6e1004dc151afbb9f02978aeb9e/fireworks-2.0.8-py3-none-any.whl", hash = "sha256:265450da71c0c96a23e35be2e9a3a7b22b6e03c6999a911c5fb687876d90660f", size = 464487, upload-time = "2025-12-05T20:40:08.036Z" }, +] + [[package]] name = "fireworks-ai" version = "0.19.19" @@ -1659,6 +1687,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/3c/ea7615940131402570b1fdf79472c1328ce71ca17b60ab16ae8ead7ef53d/fireworks_ai-0.19.19-py3-none-any.whl", hash = "sha256:a375304c4e1fa8f2e8d32b8edf53bdc4eb9f55cd0e9085c0866e479aaa1880a1", size = 570660, upload-time = "2025-09-09T22:08:49.387Z" }, ] +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "flask-paginate" +version = "2024.4.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/d0/aca9153b109f0062eaadb497448f5e596f87cc89474d77347a1d931c8842/flask-paginate-2024.4.12.tar.gz", hash = "sha256:2de04606b061736f0fc8fbe73d9d4d6fc03664638eca70a57728b03b3e2c9577", size = 9727, upload-time = "2024-04-12T02:17:46.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/8d3948fc5b01d4c65bfadc794cfec6f75aeab375b4245b809607458d60b8/flask_paginate-2024.4.12-py2.py3-none-any.whl", hash = "sha256:2c5a19c851b07456b2bb354d2f5dc9d59fbad0b7241599f3450c86c2427e4da1", size = 7598, upload-time = "2024-04-12T02:17:43.675Z" }, +] + [[package]] name = "fqdn" version = "1.5.1" @@ -2063,6 +2120,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/8b/ad381ec1b8195fa4a9a693cb8087e031b99530c0d6b8ad036dcb99e144c4/grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654", size = 76311, upload-time = "2025-05-04T16:27:22.818Z" }, ] +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + [[package]] name = "gymnasium" version = "1.2.0" @@ -2462,6 +2531,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jaconv" version = "0.4.0" @@ -3368,6 +3446,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/71/4ad9a42f2772793a03cb698f0fc42499f04e6e8d2560ba2f7da0fb059a8e/mmh3-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b22fe2e54be81f6c07dcb36b96fa250fb72effe08aa52fbb83eade6e1e2d5fd7", size = 38890, upload-time = "2025-01-25T08:39:25.28Z" }, ] +[[package]] +name = "monty" +version = "2025.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/54/a1b2b4c9b16ebc5ea72cf22b1a6bb7ceaa79187fbf22a404c9677c8f90dd/monty-2025.3.3.tar.gz", hash = "sha256:16c1eb54b2372e765c2f3f14cff01cc76ab55c3cc12b27d49962fb8072310ae0", size = 85592, upload-time = "2025-03-03T21:12:44.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/df/b3a36544734be3ac0eacf11bcfb8609464dd07d8bad0dff6e46109c68002/monty-2025.3.3-py3-none-any.whl", hash = "sha256:5eadb6d748c007bc63c34eceb2d80faff18f3996121d261dbceeea22adc58775", size = 51925, upload-time = "2025-03-03T21:12:42.598Z" }, +] + [[package]] name = "more-itertools" version = "10.7.0" @@ -5297,6 +5389,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/e8/11644fe823e05c583b330e9fb81e3e8fc5d079036512a8300fc157be349d/pykakasi-2.3.0-py3-none-any.whl", hash = "sha256:26d21b090048ff45c6a4d8e962426b7951767216008ec30358e8a9d74af77f29", size = 2395003, upload-time = "2024-06-24T04:57:18.101Z" }, ] +[[package]] +name = "pymongo" +version = "4.15.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/a0/5c324fe6735b2bc189779ff46e981a59d495a74594f45542159125d77256/pymongo-4.15.5.tar.gz", hash = "sha256:3a8d6bf2610abe0c97c567cf98bf5bba3e90ccc93cc03c9dde75fa11e4267b42", size = 2471889, upload-time = "2025-12-02T18:44:30.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/e4/d80061be4e53125597dd2916171c87986043b190e50c1834fff455e71d42/pymongo-4.15.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a01a2054d50b50c121c720739a2216d855c48726b0002894de9b991cdd68a2a5", size = 811318, upload-time = "2025-12-02T18:42:12.09Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b3/c499fe0814e4d3a84fa3ff5df5133bf847529d8b5a051e6108b5a25b75c7/pymongo-4.15.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e57968139d81367117ed7b75d921445a575d4d7e61536f5e860475df92ac0a9", size = 811676, upload-time = "2025-12-02T18:42:14.396Z" }, + { url = "https://files.pythonhosted.org/packages/62/71/8e21a8a680546b3a90afbb878a16fe2a7cb0f7d9652aa675c172e57856a1/pymongo-4.15.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:266aa37e3673e5dcfdd359a81d27131fc133e49cf8e5d9f9f27a5845fac2cd1f", size = 1185485, upload-time = "2025-12-02T18:42:16.147Z" }, + { url = "https://files.pythonhosted.org/packages/03/56/bdc292a7b01aa2aba806883dbcacc3be837d65425453aa2bc27954ba5a55/pymongo-4.15.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2883da6bd0545cc2f12672f6a609b33d48e099a220872ca2bf9bf29fe96a32c3", size = 1203866, upload-time = "2025-12-02T18:42:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e2/12bebc7e93a81c2f804ffcc94997f61f0e2cd2c11bf0f01da8e0e1425e5c/pymongo-4.15.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2fc32b354a608ec748d89bbe236b74b967890667eea1af54e92dfd8fbf26df52", size = 1242550, upload-time = "2025-12-02T18:42:19.898Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ac/c48f6f59a660ec44052ee448dea1c71da85cfaa4a0c17c726d4ee2db7716/pymongo-4.15.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c006cbaa4b40d296dd2bb8828976866c876ead4c39032b761dcf26f1ba56fde", size = 1232844, upload-time = "2025-12-02T18:42:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/6368befca7a2f3b51460755a373f78b72003aeee95e8e138cbd479c307f4/pymongo-4.15.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce21e3dc5939b83d03f871090d83ac29fef055bd057f8d3074b6cad10f86b04c", size = 1200192, upload-time = "2025-12-02T18:42:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/bc810a017ebb20e6e301fa8c5b21c5e53691fdde2cfd39bd9c450e957b14/pymongo-4.15.5-cp310-cp310-win32.whl", hash = "sha256:1b545dcf66a9f06e9b501bfb0438e1eb9af67336e8a5cf36c4bc0a5d3fbe7a37", size = 798338, upload-time = "2025-12-02T18:42:25.438Z" }, + { url = "https://files.pythonhosted.org/packages/46/17/3be0b476a6bfb3a51bf1750323b5eddf883dddb6482ccb8dbcab2c6c48ad/pymongo-4.15.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ecc544f515f828f05d3c56cd98063ba3ef8b75f534c63de43306d59f1e93fcd", size = 808153, upload-time = "2025-12-02T18:42:26.889Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/39f9daf16d695abd58987bb5e2c164b5a64e42b8d53d3c43bc06e4aa7dfc/pymongo-4.15.5-cp310-cp310-win_arm64.whl", hash = "sha256:1151968ab90db146f0591b6c7db27ce4f73c7ffa0bbddc1d7fb7cb14c9f0b967", size = 800943, upload-time = "2025-12-02T18:42:28.668Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/e43387c2ed78a60ad917c45f4d4de4f6992929d63fe15af4c2e624f093a9/pymongo-4.15.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:57157a4b936e28e2fbe7017b2f6a751da5e284675cab371f2c596d4e0e4f58f3", size = 865894, upload-time = "2025-12-02T18:42:30.496Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8c/f2c9c55adb9709a4b2244d8d8d9ec05e4abb274e03fe8388b58a34ae08b0/pymongo-4.15.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2a34a7391f4cc54fc584e49db6f7c3929221a9da08b3af2d2689884a5943843", size = 866235, upload-time = "2025-12-02T18:42:31.862Z" }, + { url = "https://files.pythonhosted.org/packages/5e/aa/bdf3553d7309b0ebc0c6edc23f43829b1758431f2f2f7385d2427b20563b/pymongo-4.15.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:be040c8cdaf9c2d5ae9ab60a67ecab453ec19d9ccd457a678053fdceab5ee4c8", size = 1429787, upload-time = "2025-12-02T18:42:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/80a8eefc88f578fde56489e5278ba5caa5ee9b6f285959ed2b98b44e2133/pymongo-4.15.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:defe93944526b1774265c16acf014689cb1b0b18eb84a7b370083b214f9e18cd", size = 1456747, upload-time = "2025-12-02T18:42:35.805Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/6a7ec290c7ab22aab117ab60e7375882ec5af7433eaf077f86e187a3a9e8/pymongo-4.15.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:816e66116f0ef868eff0463a8b28774af8b547466dbad30c8e82bf0325041848", size = 1514670, upload-time = "2025-12-02T18:42:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/65/8a/5822aa20b274ee8a8821bf0284f131e7fc555b0758c3f2a82c51ae73a3c6/pymongo-4.15.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66c7b332532e0f021d784d04488dbf7ed39b7e7d6d5505e282ec8e9cf1025791", size = 1500711, upload-time = "2025-12-02T18:42:39.61Z" }, + { url = "https://files.pythonhosted.org/packages/32/ca/63984e32b4d745a25445c9da1159dfe4568a03375f32bb1a9e009dccb023/pymongo-4.15.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:acc46a9e47efad8c5229e644a3774169013a46ee28ac72d1fa4edd67c0b7ee9b", size = 1452021, upload-time = "2025-12-02T18:42:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/f1/23/0d6988f3fdfcacae2ac8d7b76eb24f80ebee9eb607c53bcebfad75b7fd85/pymongo-4.15.5-cp311-cp311-win32.whl", hash = "sha256:b9836c28ba350d8182a51f32ef9bb29f0c40e82ba1dfb9e4371cd4d94338a55d", size = 844483, upload-time = "2025-12-02T18:42:42.814Z" }, + { url = "https://files.pythonhosted.org/packages/8e/04/dedff8a5a9539e5b6128d8d2458b9c0c83ebd38b43389620a0d97223f114/pymongo-4.15.5-cp311-cp311-win_amd64.whl", hash = "sha256:3a45876c5c2ab44e2a249fb542eba2a026f60d6ab04c7ef3924eae338d9de790", size = 859194, upload-time = "2025-12-02T18:42:45.025Z" }, + { url = "https://files.pythonhosted.org/packages/67/e5/fb6f49bceffe183e66831c2eebd2ea14bd65e2816aeaf8e2fc018fd8c344/pymongo-4.15.5-cp311-cp311-win_arm64.whl", hash = "sha256:e4a48fc5c712b3db85c9987cfa7fde0366b7930018de262919afd9e52cfbc375", size = 848377, upload-time = "2025-12-02T18:42:47.19Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/8f9fcb2dc9eab1fb0ed02da31e7f4847831d9c0ef08854a296588b97e8ed/pymongo-4.15.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c33477af1a50d1b4d86555e098fc2cf5992d839ad538dea0c00a8682162b7a75", size = 920955, upload-time = "2025-12-02T18:42:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/c0808bed1f82b3008909b9562615461e59c3b66f8977e502ea87c88b08a4/pymongo-4.15.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e6b30defa4a52d3698cd84d608963a8932f7e9b6ec5130087e7082552ac685e5", size = 920690, upload-time = "2025-12-02T18:42:50.832Z" }, + { url = "https://files.pythonhosted.org/packages/12/f3/feea83150c6a0cd3b44d5f705b1c74bff298a36f82d665f597bf89d42b3f/pymongo-4.15.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:45fec063f5672e6173bcb09b492431e3641cc74399c2b996fcb995881c2cac61", size = 1690351, upload-time = "2025-12-02T18:42:53.402Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4e/15924d33d8d429e4c41666090017c6ac5e7ccc4ce5e435a2df09e45220a8/pymongo-4.15.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c6813110c0d9fde18674b7262f47a2270ae46c0ddd05711e6770caa3c9a3fb", size = 1726089, upload-time = "2025-12-02T18:42:56.187Z" }, + { url = "https://files.pythonhosted.org/packages/a5/49/650ff29dc5f9cf090dfbd6fb248c56d8a10d268b6f46b10fb02fbda3c762/pymongo-4.15.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8ec48d1db9f44c737b13be4299a1782d5fde3e75423acbbbe927cb37ebbe87d", size = 1800637, upload-time = "2025-12-02T18:42:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/f34661ade670ee42331543f4aa229569ac7ef45907ecda41b777137b9f40/pymongo-4.15.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f410694fdd76631ead7df6544cdeadaf2407179196c3642fced8e48bb21d0a6", size = 1785480, upload-time = "2025-12-02T18:43:00.626Z" }, + { url = "https://files.pythonhosted.org/packages/10/b6/378bb26937f6b366754484145826aca2d2361ac05b0bacd45a35876abcef/pymongo-4.15.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8c46765d6ac5727a899190aacdeec7a57f8c93346124ddd7e12633b573e2e65", size = 1718548, upload-time = "2025-12-02T18:43:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/31b8afba36f794a049633e105e45c30afaa0e1c0bab48332d999e87d4860/pymongo-4.15.5-cp312-cp312-win32.whl", hash = "sha256:647118a58dca7d3547714fc0b383aebf81f5852f4173dfd77dd34e80eea9d29b", size = 891319, upload-time = "2025-12-02T18:43:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/a7e6d8c5657d922872ac75ab1c0a1335bfb533d2b4dad082d5d04089abbb/pymongo-4.15.5-cp312-cp312-win_amd64.whl", hash = "sha256:099d3e2dddfc75760c6a8fadfb99c1e88824a99c2c204a829601241dff9da049", size = 910919, upload-time = "2025-12-02T18:43:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b4/286c12fa955ae0597cd4c763d87c986e7ade681d4b11a81766f62f079c79/pymongo-4.15.5-cp312-cp312-win_arm64.whl", hash = "sha256:649cb906882c4058f467f334fb277083998ba5672ffec6a95d6700db577fd31a", size = 896357, upload-time = "2025-12-02T18:43:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/9b/92/e70db1a53bc0bb5defe755dee66b5dfbe5e514882183ffb696d6e1d38aa2/pymongo-4.15.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b736226f9001bbbd02f822acb9b9b6d28319f362f057672dfae2851f7da6125", size = 975324, upload-time = "2025-12-02T18:43:11.074Z" }, + { url = "https://files.pythonhosted.org/packages/a4/90/dd78c059a031b942fa36d71796e94a0739ea9fb4251fcd971e9579192611/pymongo-4.15.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60ea9f07fbbcc7c88f922082eb27436dce6756730fdef76a3a9b4c972d0a57a3", size = 975129, upload-time = "2025-12-02T18:43:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/87cf1bb75ef296456912eb7c6d51ebe7a36dbbe9bee0b8a9cd02a62a8a6e/pymongo-4.15.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20af63218ae42870eaee31fb8cc4ce9e3af7f04ea02fc98ad751fb7a9c8d7be3", size = 1950973, upload-time = "2025-12-02T18:43:15.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/68/dfa507c8e5cebee4e305825b436c34f5b9ba34488a224b7e112a03dbc01e/pymongo-4.15.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20d9c11625392f1f8dec7688de5ce344e110ca695344efa313ae4839f13bd017", size = 1995259, upload-time = "2025-12-02T18:43:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/85/9d/832578e5ed7f682a09441bbc0881ffd506b843396ef4b34ec53bd38b2fb2/pymongo-4.15.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1202b3e5357b161acb7b7cc98e730288a5c15544e5ef7254b33931cb9a27c36e", size = 2086591, upload-time = "2025-12-02T18:43:19.559Z" }, + { url = "https://files.pythonhosted.org/packages/0a/99/ca8342a0cefd2bb1392187ef8fe01432855e3b5cd1e640495246bcd65542/pymongo-4.15.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63af710e9700dbf91abccf119c5f5533b9830286d29edb073803d3b252862c0d", size = 2070200, upload-time = "2025-12-02T18:43:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7d/f4a9c1fceaaf71524ff9ff964cece0315dcc93df4999a49f064564875bff/pymongo-4.15.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22eeb86861cf7b8ee6886361d52abb88e3cd96c6f6d102e45e2604fc6e9e316", size = 1985263, upload-time = "2025-12-02T18:43:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/f942535bcc6e22d3c26c7e730daf296ffe69d8ce474c430ea7e551f8cf33/pymongo-4.15.5-cp313-cp313-win32.whl", hash = "sha256:aad6efe82b085bf77cec2a047ded2c810e93eced3ccf1a8e3faec3317df3cd52", size = 938143, upload-time = "2025-12-02T18:43:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/c92a6927d676dd376d1ae05c680139c5cad068b22e5f0c8cb61014448894/pymongo-4.15.5-cp313-cp313-win_amd64.whl", hash = "sha256:ccc801f6d71ebee2ec2fb3acc64b218fa7cdb7f57933b2f8eee15396b662a0a0", size = 962603, upload-time = "2025-12-02T18:43:27.816Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f0/cdf78e9ed9c26fb36b8d75561ebf3c7fe206ff1c3de2e1b609fccdf3a55b/pymongo-4.15.5-cp313-cp313-win_arm64.whl", hash = "sha256:f043abdf20845bf29a554e95e4fe18d7d7a463095d6a1547699a12f80da91e02", size = 944308, upload-time = "2025-12-02T18:43:29.371Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/49713e0f8f41110e8b2bcce7c88570b158cf43dd53a0d01d4e1c772c7ede/pymongo-4.15.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba0e75a390334221744e2666fd2d4c82419b580c9bc8d6e0d2d61459d263f3af", size = 1029996, upload-time = "2025-12-02T18:43:31.58Z" }, + { url = "https://files.pythonhosted.org/packages/23/de/1df5d7b49647e9e4511054f750c1109cb8e160763b286b96879917170618/pymongo-4.15.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:853ec7da97642eabaf94d3de4453a86365729327d920af167bf14b2e87b24dce", size = 1029612, upload-time = "2025-12-02T18:43:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/3a051228e5beb0b421d725bb2ab5207a260c718d9b5be5b85cfe963733e3/pymongo-4.15.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7631304106487480ebbd8acbe44ff1e69d1fdc27e83d9753dc1fd227cea10761", size = 2211814, upload-time = "2025-12-02T18:43:35.769Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b3/989531a056c4388ef18245d1a6d6b3ec5c538666b000764286119efbf194/pymongo-4.15.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50505181365eba5d4d35c462870b3614c8eddd0b2407c89377c1a59380640dd9", size = 2264629, upload-time = "2025-12-02T18:43:37.479Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5f/8b3339fec44d0ba6d9388a19340fb1534c85ab6aa9fd8fb9c1af146bb72a/pymongo-4.15.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b75ec7006471299a571d6db1c5609ea4aa9c847a701e9b2953a8ede705d82db", size = 2371823, upload-time = "2025-12-02T18:43:39.866Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/706bf45cf12990b6cb73e6290b048944a51592de7a597052a761eea90b8d/pymongo-4.15.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c3fc24cb1f4ec60ed83162d4bba0c26abc6c9ae78c928805583673f3b3ea6984", size = 2351860, upload-time = "2025-12-02T18:43:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c5/fdcc81c20c67a61ba1073122c9ab42c937dd6f914004747e9ceefa4cead3/pymongo-4.15.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21d17bb2934b0640863361c08dd06991f128a97f9bee19425a499227be9ae6b4", size = 2251349, upload-time = "2025-12-02T18:43:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1c/e540ccac0685b234a23574dce3c8e077cd59bcb73ab19bcab1915894d3a6/pymongo-4.15.5-cp314-cp314-win32.whl", hash = "sha256:5a3974236cb842b4ef50a5a6bfad9c7d83a713af68ea3592ba240bbcb863305a", size = 992901, upload-time = "2025-12-02T18:43:45.732Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/eb72c53bc897cb50b57000d71ce9bdcfc9c84ba4c7f6d55348df47b241d8/pymongo-4.15.5-cp314-cp314-win_amd64.whl", hash = "sha256:73fa8a7eee44fd95ba7d5cf537340ff3ff34efeb1f7d6790532d0a6ed4dee575", size = 1021205, upload-time = "2025-12-02T18:43:47.756Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4a/74a7cc350d60953d27b5636906b43b232b501cee07f70f6513ac603097e8/pymongo-4.15.5-cp314-cp314-win_arm64.whl", hash = "sha256:d41288ca2a3eb9ac7c8cad4ea86ef8d63b69dc46c9b65c2bbd35331ec2a0fc57", size = 1000616, upload-time = "2025-12-02T18:43:49.677Z" }, + { url = "https://files.pythonhosted.org/packages/1a/22/1e557868b9b207d7dbf7706412251b28a82d4b958e007b6f2569d59ada3d/pymongo-4.15.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:552670f0c8bff103656d4e4b1f2c018f789c9de03f7615ed5e547d5b1b83cda0", size = 1086723, upload-time = "2025-12-02T18:43:51.432Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9c/2e24c2da289e1d3b9bc4e0850136a364473bddfbe8b19b33d2bb5d30ee0d/pymongo-4.15.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41891b45f6ff1e23cfd1b7fbe40286664ad4507e2d2aa61c6d8c40eb6e11dded", size = 1086653, upload-time = "2025-12-02T18:43:53.131Z" }, + { url = "https://files.pythonhosted.org/packages/c6/be/4c2460c9ec91a891c754b91914ce700cc46009dae40183a85e26793dfae9/pymongo-4.15.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:524a8a593ae2eb1ec6db761daf0c03f98824e9882ab7df3d458d0c76c7ade255", size = 2531627, upload-time = "2025-12-02T18:43:55.141Z" }, + { url = "https://files.pythonhosted.org/packages/a0/48/cea56d04eb6bbd8b8943ff73d7cf26b94f715fccb23cf7ef9a4f853725a0/pymongo-4.15.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7ceb35c41b86711a1b284c604e2b944a2d46cb1b8dd3f8b430a9155491378f2", size = 2603767, upload-time = "2025-12-02T18:43:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ff/6743e351f8e0d5c3f388deb15f0cdbb77d2439eb3fba7ebcdf7878719517/pymongo-4.15.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3be2336715924be3a861b5e40c634376fd6bfe6dd1892d391566aa5a88a31307", size = 2725216, upload-time = "2025-12-02T18:43:59.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/fa532b6320b3ba61872110ff6f674bd54b54a592c0c64719e4f46852d0b6/pymongo-4.15.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d65df9c015e33f74ea9d1abf474971abca21e347a660384f8227dbdab75a33ca", size = 2704804, upload-time = "2025-12-02T18:44:01.415Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/1905c269aced043973b9528d94678e62e2eba249e70490c3c32dc70e2501/pymongo-4.15.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83c05bea05e151754357f8e6bbb80d5accead5110dc58f64e283173c71ec9de2", size = 2582274, upload-time = "2025-12-02T18:44:03.427Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/78c13179961e418396ec6ef53c0f1c855f1e9f1176d10909e8345d65366a/pymongo-4.15.5-cp314-cp314t-win32.whl", hash = "sha256:7c285614a3e8570b03174a25db642e449b0e7f77a6c9e487b73b05c9bf228ee6", size = 1044015, upload-time = "2025-12-02T18:44:05.318Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d5/49012f03418dce976124da339f3a6afbe6959cb0468ca6302596fe272926/pymongo-4.15.5-cp314-cp314t-win_amd64.whl", hash = "sha256:aae7d96f7b2b1a2753349130797543e61e93ee2ace8faa7fbe0565e2eb5d815f", size = 1078481, upload-time = "2025-12-02T18:44:07.215Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fc/f352a070d8ff6f388ce344c5ddb82348a38e0d1c99346fa6bfdef07134fe/pymongo-4.15.5-cp314-cp314t-win_arm64.whl", hash = "sha256:576a7d4b99465d38112c72f7f3d345f9d16aeeff0f923a3b298c13e15ab4f0ad", size = 1051166, upload-time = "2025-12-02T18:44:09.048Z" }, +] + [[package]] name = "pyperclip" version = "1.9.0" @@ -6077,6 +6240,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] +[[package]] +name = "ruamel-yaml" +version = "0.18.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/c7/ee630b29e04a672ecfc9b63227c87fd7a37eb67c1bf30fe95376437f897c/ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a", size = 147269, upload-time = "2025-10-22T17:54:02.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba", size = 119858, upload-time = "2025-10-22T17:53:59.012Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, + { url = "https://files.pythonhosted.org/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, + { url = "https://files.pythonhosted.org/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, + { url = "https://files.pythonhosted.org/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +] + [[package]] name = "ruff" version = "0.9.10" @@ -6412,6 +6645,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" }, ] +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + [[package]] name = "temporalio" version = "1.17.0" From 97f056b834d7cd92ade11318f5a56d7f1fca99f5 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 16 Dec 2025 15:48:50 -0800 Subject: [PATCH 03/16] wrong package --- pyproject.toml | 2 +- uv.lock | 760 +++++++++++-------------------------------------- 2 files changed, 166 insertions(+), 596 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52bc8c1b..86022c02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "pytest-asyncio>=0.21.0", "peewee>=3.18.2", "backoff>=2.2.0", - "fireworks>=1.0.0a11", + "fireworks-ai>=1.0.0a11", "questionary>=2.0.0", # Dependencies for vendored tau2 package "toml>=0.10.0", diff --git a/uv.lock b/uv.lock index 62cab335..65770268 100644 --- a/uv.lock +++ b/uv.lock @@ -48,18 +48,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/00/40c6b0313c25d1ab6fac2ecba1cd5b15b1cd3c3a71b3d267ad890e405889/ag_ui_protocol-0.1.8-py3-none-any.whl", hash = "sha256:1567ccb067b7b8158035b941a985e7bb185172d660d4542f3f9c6fff77b55c6e", size = 7066, upload-time = "2025-07-15T10:55:35.075Z" }, ] -[[package]] -name = "aiodns" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycares" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/0a/163e5260cecc12de6abc259d158d9da3b8ec062ab863107dcdb1166cdcef/aiodns-3.5.0.tar.gz", hash = "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", size = 14380, upload-time = "2025-06-13T16:21:53.595Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/2c/711076e5f5d0707b8ec55a233c8bfb193e0981a800cd1b3b123e8ff61ca1/aiodns-3.5.0-py3-none-any.whl", hash = "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5", size = 8068, upload-time = "2025-06-13T16:21:52.45Z" }, -] - [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -155,13 +143,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098, upload-time = "2025-07-10T13:04:53.999Z" }, ] -[package.optional-dependencies] -speedups = [ - { name = "aiodns" }, - { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, - { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, -] - [[package]] name = "aiosignal" version = "1.4.0" @@ -329,15 +310,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721, upload-time = "2023-08-10T16:35:55.203Z" }, ] -[[package]] -name = "asyncstdlib-fw" -version = "3.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/cf/f1ed414c9e63ec825f877406dffb4ab504153a68b6f099c7e079714cecca/asyncstdlib_fw-3.13.2.tar.gz", hash = "sha256:51ad094c204c5936c3040f38c32fd6d549b391c980f21f1fa095b65fb6806a1f", size = 37420, upload-time = "2025-05-19T16:23:51.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/6e/3b97da4a8b7bf655f3bb6e55a8bd46b840b5bd14606483d3d2b7e60dbde2/asyncstdlib_fw-3.13.2-py3-none-any.whl", hash = "sha256:91cc2b19c232cd377f26eec0f353468eec7d80cf83709e1ca903bd954a694b27", size = 44941, upload-time = "2025-05-19T16:23:50.085Z" }, -] - [[package]] name = "attrs" version = "23.2.0" @@ -420,26 +392,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, ] -[[package]] -name = "betterproto-fw" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpclib" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/b2/d91fd772f532c735a37ecefaacb9ee56fd7516730508e21bb048ee2dd0be/betterproto_fw-2.0.3.tar.gz", hash = "sha256:bade467215224f2807842da18fe81258a0a856766bad5f0a20a1474c5cdb6b93", size = 285426, upload-time = "2025-05-18T19:36:43.591Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/85/fdd477aa981bf80ff610bc256d3c5b079a4294cab74ff6e13cf832bbdaa1/betterproto_fw-2.0.3-py3-none-any.whl", hash = "sha256:af66a24e5d182922121b7119947166289442b19d2664d057e0b0a96c1f049757", size = 105059, upload-time = "2025-05-18T19:36:41.36Z" }, -] - -[package.optional-dependencies] -compiler = [ - { name = "jinja2" }, - { name = "ruff" }, -] - [[package]] name = "bleach" version = "6.2.0" @@ -457,15 +409,6 @@ css = [ { name = "tinycss2" }, ] -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - [[package]] name = "boto3" version = "1.40.17" @@ -527,98 +470,6 @@ otel = [ { name = "opentelemetry-sdk" }, ] -[[package]] -name = "brotli" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045, upload-time = "2023-09-07T14:03:16.894Z" }, - { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218, upload-time = "2023-09-07T14:03:18.917Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872, upload-time = "2023-09-07T14:03:20.398Z" }, - { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254, upload-time = "2023-09-07T14:03:21.914Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293, upload-time = "2023-09-07T14:03:24Z" }, - { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385, upload-time = "2023-09-07T14:03:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104, upload-time = "2023-09-07T14:03:27.849Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981, upload-time = "2023-09-07T14:03:29.92Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297, upload-time = "2023-09-07T14:03:32.035Z" }, - { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735, upload-time = "2023-09-07T14:03:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107, upload-time = "2024-10-18T12:32:09.016Z" }, - { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400, upload-time = "2024-10-18T12:32:11.134Z" }, - { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985, upload-time = "2024-10-18T12:32:12.813Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099, upload-time = "2024-10-18T12:32:14.733Z" }, - { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172, upload-time = "2023-09-07T14:03:35.212Z" }, - { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255, upload-time = "2023-09-07T14:03:36.447Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload-time = "2023-09-07T14:03:37.779Z" }, - { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload-time = "2023-09-07T14:03:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload-time = "2023-09-07T14:03:40.858Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950, upload-time = "2023-09-07T14:03:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527, upload-time = "2023-09-07T14:03:44.552Z" }, - { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489, upload-time = "2023-09-07T14:03:46.594Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080, upload-time = "2023-09-07T14:03:48.204Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051, upload-time = "2023-09-07T14:03:50.348Z" }, - { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172, upload-time = "2023-09-07T14:03:52.395Z" }, - { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023, upload-time = "2023-09-07T14:03:53.96Z" }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871, upload-time = "2024-10-18T12:32:16.688Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784, upload-time = "2024-10-18T12:32:18.459Z" }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905, upload-time = "2024-10-18T12:32:20.192Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467, upload-time = "2024-10-18T12:32:21.774Z" }, - { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169, upload-time = "2023-09-07T14:03:55.404Z" }, - { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253, upload-time = "2023-09-07T14:03:56.643Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload-time = "2024-10-18T12:32:23.824Z" }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload-time = "2024-10-18T12:32:25.641Z" }, - { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload-time = "2023-09-07T14:03:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload-time = "2023-09-07T14:03:59.319Z" }, - { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload-time = "2023-09-07T14:04:01.327Z" }, - { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload-time = "2023-09-07T14:04:03.033Z" }, - { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload-time = "2023-09-07T14:04:04.675Z" }, - { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload-time = "2023-09-07T14:04:06.585Z" }, - { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload-time = "2023-09-07T14:04:08.668Z" }, - { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload-time = "2023-09-07T14:04:10.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload-time = "2023-09-07T14:04:12.875Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload-time = "2023-09-07T14:04:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload-time = "2024-10-18T12:32:27.257Z" }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload-time = "2024-10-18T12:32:29.376Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload-time = "2024-10-18T12:32:31.371Z" }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload-time = "2024-10-18T12:32:33.293Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276, upload-time = "2023-09-07T14:04:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255, upload-time = "2023-09-07T14:04:17.83Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" }, - { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload-time = "2024-10-18T12:32:52.661Z" }, - { 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 = "brotlicffi" -version = "1.1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192, upload-time = "2023-09-14T14:22:40.707Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786, upload-time = "2023-09-14T14:21:57.72Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165, upload-time = "2023-09-14T14:21:59.613Z" }, - { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895, upload-time = "2023-09-14T14:22:01.22Z" }, - { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834, upload-time = "2023-09-14T14:22:03.571Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731, upload-time = "2023-09-14T14:22:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783, upload-time = "2023-09-14T14:22:07.096Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3b/bd4f3d2bcf2306ae66b0346f5b42af1962480b200096ffc7abc3bd130eca/brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca", size = 397397, upload-time = "2023-09-14T14:22:08.519Z" }, - { url = "https://files.pythonhosted.org/packages/54/10/1fd57864449360852c535c2381ee7120ba8f390aa3869df967c44ca7eba1/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391", size = 379698, upload-time = "2023-09-14T14:22:10.52Z" }, - { url = "https://files.pythonhosted.org/packages/e5/95/15aa422aa6450e6556e54a5fd1650ff59f470aed77ac739aa90ab63dc611/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8", size = 378635, upload-time = "2023-09-14T14:22:11.982Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a7/f254e13b2cb43337d6d99a4ec10394c134e41bfda8a2eff15b75627f4a3d/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35", size = 385719, upload-time = "2023-09-14T14:22:13.483Z" }, - { url = "https://files.pythonhosted.org/packages/72/a9/0971251c4427c14b2a827dba3d910d4d3330dabf23d4278bf6d06a978847/brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d", size = 361760, upload-time = "2023-09-14T14:22:14.767Z" }, -] - [[package]] name = "build" version = "1.2.2.post1" @@ -1230,7 +1081,7 @@ dependencies = [ { name = "deepdiff" }, { name = "docstring-parser" }, { name = "fastapi" }, - { name = "fireworks" }, + { name = "fireworks-ai" }, { name = "httpx" }, { name = "hydra-core" }, { name = "litellm" }, @@ -1371,7 +1222,7 @@ requires-dist = [ { name = "docstring-parser", specifier = ">=0.15" }, { name = "e2b", marker = "extra == 'dev'" }, { name = "fastapi", specifier = ">=0.116.1" }, - { name = "fireworks", specifier = ">=1.0.0a11" }, + { name = "fireworks-ai", specifier = ">=1.0.0a11" }, { name = "google-auth", marker = "extra == 'bigquery'", specifier = ">=2.0.0" }, { name = "google-cloud-bigquery", marker = "extra == 'bigquery'", specifier = ">=3.0.0" }, { name = "gymnasium", marker = "extra == 'dev'", specifier = ">=1.2.0" }, @@ -1638,82 +1489,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] -[[package]] -name = "fireworks" -version = "2.0.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "flask-paginate" }, - { name = "gunicorn" }, - { name = "jinja2" }, - { name = "monty" }, - { name = "pymongo" }, - { name = "python-dateutil" }, - { name = "ruamel-yaml" }, - { name = "tabulate" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/18/d020950eb8566d5eceffbde8597cc31405d6bfc327088212e5d95cf8a556/fireworks-2.0.8.tar.gz", hash = "sha256:2c7cc91baa2eb3f3518d78e97a78bc46116885d72d465e4aeef9b0dca320187a", size = 5026459, upload-time = "2025-12-05T20:40:09.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/03/cf5fd9be0a54f632b93d49cb3da72242e6e1004dc151afbb9f02978aeb9e/fireworks-2.0.8-py3-none-any.whl", hash = "sha256:265450da71c0c96a23e35be2e9a3a7b22b6e03c6999a911c5fb687876d90660f", size = 464487, upload-time = "2025-12-05T20:40:08.036Z" }, -] - [[package]] name = "fireworks-ai" -version = "0.19.19" +version = "1.0.0a11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp", extra = ["speedups"] }, - { name = "asyncstdlib-fw" }, - { name = "attrs" }, - { name = "betterproto-fw", extra = ["compiler"] }, - { name = "googleapis-common-protos" }, - { name = "grpcio" }, + { name = "aiohttp" }, + { name = "anyio" }, + { name = "distro" }, { name = "httpx" }, - { name = "httpx-sse" }, - { name = "httpx-ws" }, - { name = "mmh3" }, - { name = "openai" }, - { name = "pillow" }, - { name = "protobuf" }, + { name = "httpx-aiohttp" }, { name = "pydantic" }, - { name = "rich" }, - { name = "toml" }, + { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/48/73a7fe68b44ca947174321557af8f53176f6564147fbc365c2f45b370742/fireworks_ai-0.19.19.tar.gz", hash = "sha256:76b4eb47b2d25f26aba5dbb2d1f152397ad5c86b3072ae93c628f025a88bebc1", size = 411948, upload-time = "2025-09-09T22:08:51.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3c/ea7615940131402570b1fdf79472c1328ce71ca17b60ab16ae8ead7ef53d/fireworks_ai-0.19.19-py3-none-any.whl", hash = "sha256:a375304c4e1fa8f2e8d32b8edf53bdc4eb9f55cd0e9085c0866e479aaa1880a1", size = 570660, upload-time = "2025-09-09T22:08:49.387Z" }, -] - -[[package]] -name = "flask" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, -] - -[[package]] -name = "flask-paginate" -version = "2024.4.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/d0/aca9153b109f0062eaadb497448f5e596f87cc89474d77347a1d931c8842/flask-paginate-2024.4.12.tar.gz", hash = "sha256:2de04606b061736f0fc8fbe73d9d4d6fc03664638eca70a57728b03b3e2c9577", size = 9727, upload-time = "2024-04-12T02:17:46.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/45/e2c792a35e9951c84655cf6f251bda30ae9c9b55f9590901ca5e2372e481/fireworks_ai-1.0.0a11.tar.gz", hash = "sha256:f1c68d8c65ab429ea6298358dcd67e7cda4da569a0a4fab04d45d9ef074623a4", size = 238148, upload-time = "2025-12-16T23:14:34.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/a0/8d3948fc5b01d4c65bfadc794cfec6f75aeab375b4245b809607458d60b8/flask_paginate-2024.4.12-py2.py3-none-any.whl", hash = "sha256:2c5a19c851b07456b2bb354d2f5dc9d59fbad0b7241599f3450c86c2427e4da1", size = 7598, upload-time = "2024-04-12T02:17:43.675Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a2/dea1ea09c23ac97820c3f09deb313ea6de4a46a5283b3374c5bd4e3f8073/fireworks_ai-1.0.0a11-py3-none-any.whl", hash = "sha256:d98693f22ec9b9f33f74596b061b5f7d27a8c65a1b1a7998b4a9c505afaca1cf", size = 300642, upload-time = "2025-12-16T23:14:33.048Z" }, ] [[package]] @@ -2016,6 +1808,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, ] +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + [[package]] name = "griffe" version = "1.12.1" @@ -2107,31 +1954,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424, upload-time = "2025-06-28T04:23:42.136Z" }, ] -[[package]] -name = "grpclib" -version = "0.4.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h2" }, - { name = "multidict" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/75/0f0d3524b38b35e5cd07334b754aa9bd0570140ad982131b04ebfa3b0374/grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a", size = 62793, upload-time = "2025-05-04T16:27:30.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/8b/ad381ec1b8195fa4a9a693cb8087e031b99530c0d6b8ad036dcb99e144c4/grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654", size = 76311, upload-time = "2025-05-04T16:27:22.818Z" }, -] - -[[package]] -name = "gunicorn" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, -] - [[package]] name = "gymnasium" version = "1.2.0" @@ -2283,27 +2105,25 @@ http2 = [ ] [[package]] -name = "httpx-sse" -version = "0.4.0" +name = "httpx-aiohttp" +version = "0.1.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/2c/b894861cecf030fb45675ea24aa55b5722e97c602a163d872fca66c5a6d8/httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c", size = 275945, upload-time = "2025-12-12T10:12:15.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/16/8d/85c9701e9af72ca132a1783e2a54364a90c6da832304416a30fc11196ab2/httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141", size = 6367, upload-time = "2025-12-12T10:12:14.018Z" }, ] [[package]] -name = "httpx-ws" -version = "0.7.2" +name = "httpx-sse" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpcore" }, - { name = "httpx" }, - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/ba/e310ccdb8f18a2b894bfacd085ef390cf6cc70bb10ff9f109d58d94f6b47/httpx_ws-0.7.2.tar.gz", hash = "sha256:93edea6c8fc313464fc287bff7d2ad20e6196b7754c76f946f73b4af79886d4e", size = 24513, upload-time = "2025-03-28T13:20:03.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/3d/2113a5c7af9a13663fa026882d0302ed4142960388536f885dacd6be7038/httpx_ws-0.7.2-py3-none-any.whl", hash = "sha256:dd7bf9dbaa96dcd5cef1af3a7e1130cfac068bebecce25a74145022f5a8427a3", size = 14424, upload-time = "2025-03-28T13:20:04.238Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, ] [[package]] @@ -2531,15 +2351,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - [[package]] name = "jaconv" version = "0.4.0" @@ -2988,21 +2799,26 @@ wheels = [ [[package]] name = "langchain" -version = "1.0.7" +version = "0.3.27" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "langchain-core" }, - { name = "langgraph" }, + { name = "langchain-text-splitters" }, + { name = "langsmith" }, { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/7a/63c041d1ee74c505e30ec882889117baa08afce2264bcb4463929bac4e94/langchain-1.0.7.tar.gz", hash = "sha256:e3f8ad742b4cdc91d728f96bd70e4688bc11ffeca3bd160c5fe9937625d541b9", size = 465198, upload-time = "2025-11-14T20:52:21.813Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f6/f4f7f3a56626fe07e2bb330feb61254dbdf06c506e6b59a536a337da51cf/langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62", size = 10233809, upload-time = "2025-07-24T14:42:32.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/4a/02c14af46fa79ce7b02a0f8af46f5905cc7e8b647a5f1a7c793c03ac5063/langchain-1.0.7-py3-none-any.whl", hash = "sha256:cf33b4d60d7a2ff7f0f313441628927853192cdbab9d6d8ce229909a868bbf12", size = 93738, upload-time = "2025-11-14T20:52:20.717Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d5/4861816a95b2f6993f1360cfb605aacb015506ee2090433a71de9cca8477/langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798", size = 1018194, upload-time = "2025-07-24T14:42:30.23Z" }, ] [[package]] name = "langchain-core" -version = "1.0.5" +version = "0.3.80" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -3013,14 +2829,14 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/61/c356e19525a210baf960968dbfb03ee38a05e05ddb41efeb32abfcb4e360/langchain_core-1.0.5.tar.gz", hash = "sha256:7ecbad9a60dde626252733a9c18c7377f4468cfe00465ffa99f5e9c6cb9b82d2", size = 778259, upload-time = "2025-11-14T16:59:27.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/49/f76647b7ba1a6f9c11b0343056ab4d3e5fc445981d205237fed882b2ad60/langchain_core-0.3.80.tar.gz", hash = "sha256:29636b82513ab49e834764d023c4d18554d3d719a185d37b019d0a8ae948c6bb", size = 583629, upload-time = "2025-11-19T22:23:18.771Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/ee/aaf2343a35080154c82ceb110e03dd00f15459bc72e518df51724cbc41a9/langchain_core-1.0.5-py3-none-any.whl", hash = "sha256:d24c0cf12cfcd96dd4bd479aa91425f3a6652226cd824228ae422a195067b74e", size = 471506, upload-time = "2025-11-14T16:59:25.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/e8/e7a090ebe37f2b071c64e81b99fb1273b3151ae932f560bb94c22f191cde/langchain_core-0.3.80-py3-none-any.whl", hash = "sha256:2141e3838d100d17dce2359f561ec0df52c526bae0de6d4f469f8026c5747456", size = 450786, upload-time = "2025-11-19T22:23:17.133Z" }, ] [[package]] name = "langchain-fireworks" -version = "1.0.0" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -3029,23 +2845,35 @@ dependencies = [ { name = "openai" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/20/7ce23e7b0a72058a9f70612293aa13ff9fc5c4fe7f5cf97dd81314fe1c53/langchain_fireworks-1.0.0.tar.gz", hash = "sha256:cc8c812c0a1199bdeaabc9210c713e9186784d83fe3c6bc4d98516959f200e8e", size = 167346, upload-time = "2025-10-17T15:36:14.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/80/78ea4a04b1170cfa7564557808fd80e4c6f812cb5655c95a0374ca79c7ac/langchain_fireworks-0.3.0.tar.gz", hash = "sha256:09db8a06cd50df07068c07c4862e87d70b0da0f7d4e1b06f062c292af61c1433", size = 20900, upload-time = "2025-04-23T14:14:32.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/50/99ac5df1b99241e0f9df142b9a48053b840639e95e64a7497d2df06167b8/langchain_fireworks-1.0.0-py3-none-any.whl", hash = "sha256:4876086af5e4a606666339bbae6db89514804d928c91ddf059c52f37eafc1d6b", size = 16865, upload-time = "2025-10-17T15:36:13.118Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/79696d5e1573a674141a44c9c59c04629e1ba25673d64a7b03f3843ae162/langchain_fireworks-0.3.0-py3-none-any.whl", hash = "sha256:ef2ea22f8cae3e654f0e1d3eb3a60c5fcd4a914643ab324507997f89f5831166", size = 17770, upload-time = "2025-04-23T14:14:31.373Z" }, ] [[package]] name = "langchain-openai" -version = "1.0.3" +version = "0.3.35" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/73/6a96bc3a48825317886fa52a2a598286d35cf0384fce5dc3e5da7be06fd0/langchain_openai-1.0.3.tar.gz", hash = "sha256:e9df56540c1118002ab5306208c4845715e9209779c8a7ac9037eded98435fdc", size = 1032676, upload-time = "2025-11-15T00:29:03.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/96/06d0d25a37e05a0ff2d918f0a4b0bf0732aed6a43b472b0b68426ce04ef8/langchain_openai-0.3.35.tar.gz", hash = "sha256:fa985fd041c3809da256a040c98e8a43e91c6d165b96dcfeb770d8bd457bf76f", size = 786635, upload-time = "2025-10-06T15:09:28.463Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/d5/c90c5478215c20ee71d8feaf676f7ffd78d0568f8c98bd83f81ce7562ed7/langchain_openai-0.3.35-py3-none-any.whl", hash = "sha256:76d5707e6e81fd461d33964ad618bd326cb661a1975cef7c1cb0703576bdada5", size = 75952, upload-time = "2025-10-06T15:09:27.137Z" }, +] + +[[package]] +name = "langchain-text-splitters" +version = "0.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/43/dcda8fd25f0b19cb2835f2f6bb67f26ad58634f04ac2d8eae00526b0fa55/langchain_text_splitters-0.3.11.tar.gz", hash = "sha256:7a50a04ada9a133bbabb80731df7f6ddac51bc9f1b9cab7fa09304d71d38a6cc", size = 46458, upload-time = "2025-08-31T23:02:58.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/de/0cb08f8732f070397233df7ad5ef461d83784ce567e7a57d5de5eb96851f/langchain_openai-1.0.3-py3-none-any.whl", hash = "sha256:18d254dbe946d9e9fe6d31416c60c8fc06513427f6e8d8c372e015345e1e17f6", size = 82536, upload-time = "2025-11-15T00:29:02.573Z" }, + { url = "https://files.pythonhosted.org/packages/58/0d/41a51b40d24ff0384ec4f7ab8dd3dcea8353c05c973836b5e289f1465d4f/langchain_text_splitters-0.3.11-py3-none-any.whl", hash = "sha256:cf079131166a487f1372c8ab5d0bfaa6c0a4291733d9c43a34a16ac9bcd6a393", size = 33845, upload-time = "2025-08-31T23:02:57.195Z" }, ] [[package]] @@ -3070,7 +2898,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.0.3" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -3080,9 +2908,9 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/55/70f2d11d33b0310d3e48d8e049825b4a34a1c822d48f6448ae548d2cd0f8/langgraph-1.0.3.tar.gz", hash = "sha256:873a6aae6be054ef52a05c463be363a46da9711405b1b14454d595f543b68335", size = 483302, upload-time = "2025-11-10T17:41:45.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/7c/a0f4211f751b8b37aae2d88c6243ceb14027ca9ebf00ac8f3b210657af6a/langgraph-1.0.1.tar.gz", hash = "sha256:4985b32ceabb046a802621660836355dfcf2402c5876675dc353db684aa8f563", size = 480245, upload-time = "2025-10-20T18:51:59.839Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/a3/fdf6ecd0e44cb02d20afe7d0fb64c748a749f4b2e011bf9a785a32642367/langgraph-1.0.3-py3-none-any.whl", hash = "sha256:4a75146f09bd0d127a724876f4244f460c4c66353a993641bd641ed710cd010f", size = 156845, upload-time = "2025-11-10T17:41:43.868Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/acc0956a0da96b25a2c5c1a85168eacf1253639a04ed391d7a7bcaae5d6c/langgraph-1.0.1-py3-none-any.whl", hash = "sha256:892f04f64f4889abc80140265cc6bd57823dd8e327a5eef4968875f2cd9013bd", size = 155415, upload-time = "2025-10-20T18:51:58.321Z" }, ] [[package]] @@ -3100,15 +2928,15 @@ wheels = [ [[package]] name = "langgraph-prebuilt" -version = "1.0.4" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/08/45857c7c65f696307834af13946a72293e6cc49141de887f0957c2eb2c46/langgraph_prebuilt-1.0.4.tar.gz", hash = "sha256:7b4f9e97a146d2d625695c3549bdb432974b80817165139ec2ec869721e72c0f", size = 142470, upload-time = "2025-11-13T19:02:14.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/2bcb992acf67713a3557e51c1955854672ec6c1abe6ba51173a87eb8d825/langgraph_prebuilt-1.0.1.tar.gz", hash = "sha256:ecbfb9024d9d7ed9652dde24eef894650aaab96bf79228e862c503e2a060b469", size = 119918, upload-time = "2025-10-20T18:49:55.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/14/a83e50129f66df783a68acb89e7b3e9c39b5c128a8748e961bc2b187f003/langgraph_prebuilt-1.0.4-py3-none-any.whl", hash = "sha256:50b1aa2b434783b6da30785568cf7155136b484750cc2ec695c0d4255db08262", size = 34414, upload-time = "2025-11-13T19:02:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/9ffd10882403020ea866e381de7f8e504a78f606a914af7f8244456c7783/langgraph_prebuilt-1.0.1-py3-none-any.whl", hash = "sha256:8c02e023538f7ef6ad5ed76219ba1ab4f6de0e31b749e4d278f57a8a95eec9f7", size = 28458, upload-time = "2025-10-20T18:49:54.723Z" }, ] [[package]] @@ -3374,92 +3202,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, ] -[[package]] -name = "mmh3" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/1b/1fc6888c74cbd8abad1292dde2ddfcf8fc059e114c97dd6bf16d12f36293/mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c", size = 33728, upload-time = "2025-01-25T08:39:43.386Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/01/9d06468928661765c0fc248a29580c760a4a53a9c6c52cf72528bae3582e/mmh3-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eaf4ac5c6ee18ca9232238364d7f2a213278ae5ca97897cafaa123fcc7bb8bec", size = 56095, upload-time = "2025-01-25T08:37:53.621Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/7b39307fc9db867b2a9a20c58b0de33b778dd6c55e116af8ea031f1433ba/mmh3-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:48f9aa8ccb9ad1d577a16104834ac44ff640d8de8c0caed09a2300df7ce8460a", size = 40512, upload-time = "2025-01-25T08:37:54.972Z" }, - { url = "https://files.pythonhosted.org/packages/4f/85/728ca68280d8ccc60c113ad119df70ff1748fbd44c89911fed0501faf0b8/mmh3-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4ba8cac21e1f2d4e436ce03a82a7f87cda80378691f760e9ea55045ec480a3d", size = 40110, upload-time = "2025-01-25T08:37:57.86Z" }, - { url = "https://files.pythonhosted.org/packages/e4/96/beaf0e301472ffa00358bbbf771fe2d9c4d709a2fe30b1d929e569f8cbdf/mmh3-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69281c281cb01994f054d862a6bb02a2e7acfe64917795c58934b0872b9ece4", size = 100151, upload-time = "2025-01-25T08:37:59.609Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ee/9381f825c4e09ffafeffa213c3865c4bf7d39771640de33ab16f6faeb854/mmh3-5.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d05ed3962312fbda2a1589b97359d2467f677166952f6bd410d8c916a55febf", size = 106312, upload-time = "2025-01-25T08:38:02.102Z" }, - { url = "https://files.pythonhosted.org/packages/67/dc/350a54bea5cf397d357534198ab8119cfd0d8e8bad623b520f9c290af985/mmh3-5.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ae6a03f4cff4aa92ddd690611168856f8c33a141bd3e5a1e0a85521dc21ea0", size = 104232, upload-time = "2025-01-25T08:38:03.852Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5d/2c6eb4a4ec2f7293b98a9c07cb8c64668330b46ff2b6511244339e69a7af/mmh3-5.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f983535b39795d9fb7336438faae117424c6798f763d67c6624f6caf2c4c01", size = 91663, upload-time = "2025-01-25T08:38:06.24Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/17030d24196f73ecbab8b5033591e5e0e2beca103181a843a135c78f4fee/mmh3-5.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d46fdd80d4c7ecadd9faa6181e92ccc6fe91c50991c9af0e371fdf8b8a7a6150", size = 99166, upload-time = "2025-01-25T08:38:07.988Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ed/54ddc56603561a10b33da9b12e95a48a271d126f4a4951841bbd13145ebf/mmh3-5.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16e976af7365ea3b5c425124b2a7f0147eed97fdbb36d99857f173c8d8e096", size = 101555, upload-time = "2025-01-25T08:38:09.821Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c3/33fb3a940c9b70908a5cc9fcc26534aff8698180f9f63ab6b7cc74da8bcd/mmh3-5.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6fa97f7d1e1f74ad1565127229d510f3fd65d931fdedd707c1e15100bc9e5ebb", size = 94813, upload-time = "2025-01-25T08:38:11.682Z" }, - { url = "https://files.pythonhosted.org/packages/61/88/c9ff76a23abe34db8eee1a6fa4e449462a16c7eb547546fc5594b0860a72/mmh3-5.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4052fa4a8561bd62648e9eb993c8f3af3bdedadf3d9687aa4770d10e3709a80c", size = 109611, upload-time = "2025-01-25T08:38:12.602Z" }, - { url = "https://files.pythonhosted.org/packages/0b/8e/27d04f40e95554ebe782cac7bddda2d158cf3862387298c9c7b254fa7beb/mmh3-5.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3f0e8ae9f961037f812afe3cce7da57abf734285961fffbeff9a4c011b737732", size = 100515, upload-time = "2025-01-25T08:38:16.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/00/504ca8f462f01048f3c87cd93f2e1f60b93dac2f930cd4ed73532a9337f5/mmh3-5.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99297f207db967814f1f02135bb7fe7628b9eacb046134a34e1015b26b06edce", size = 100177, upload-time = "2025-01-25T08:38:18.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1d/2efc3525fe6fdf8865972fcbb884bd1f4b0f923c19b80891cecf7e239fa5/mmh3-5.1.0-cp310-cp310-win32.whl", hash = "sha256:2e6c8dc3631a5e22007fbdb55e993b2dbce7985c14b25b572dd78403c2e79182", size = 40815, upload-time = "2025-01-25T08:38:19.176Z" }, - { url = "https://files.pythonhosted.org/packages/38/b5/c8fbe707cb0fea77a6d2d58d497bc9b67aff80deb84d20feb34d8fdd8671/mmh3-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e4e8c7ad5a4dddcfde35fd28ef96744c1ee0f9d9570108aa5f7e77cf9cfdf0bf", size = 41479, upload-time = "2025-01-25T08:38:21.098Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f1/663e16134f913fccfbcea5b300fb7dc1860d8f63dc71867b013eebc10aec/mmh3-5.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:45da549269883208912868a07d0364e1418d8292c4259ca11699ba1b2475bd26", size = 38883, upload-time = "2025-01-25T08:38:22.013Z" }, - { url = "https://files.pythonhosted.org/packages/56/09/fda7af7fe65928262098382e3bf55950cfbf67d30bf9e47731bf862161e9/mmh3-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b529dcda3f951ff363a51d5866bc6d63cf57f1e73e8961f864ae5010647079d", size = 56098, upload-time = "2025-01-25T08:38:22.917Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/84c7bc3f366d6f3bd8b5d9325a10c367685bc17c26dac4c068e2001a4671/mmh3-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db1079b3ace965e562cdfc95847312f9273eb2ad3ebea983435c8423e06acd7", size = 40513, upload-time = "2025-01-25T08:38:25.079Z" }, - { url = "https://files.pythonhosted.org/packages/4f/21/25ea58ca4a652bdc83d1528bec31745cce35802381fb4fe3c097905462d2/mmh3-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22d31e3a0ff89b8eb3b826d6fc8e19532998b2aa6b9143698043a1268da413e1", size = 40112, upload-time = "2025-01-25T08:38:25.947Z" }, - { url = "https://files.pythonhosted.org/packages/bd/78/4f12f16ae074ddda6f06745254fdb50f8cf3c85b0bbf7eaca58bed84bf58/mmh3-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2139bfbd354cd6cb0afed51c4b504f29bcd687a3b1460b7e89498329cc28a894", size = 102632, upload-time = "2025-01-25T08:38:26.939Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/8f09dc999cf2a09b6138d8d7fc734efb7b7bfdd9adb9383380941caadff0/mmh3-5.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c8105c6a435bc2cd6ea2ef59558ab1a2976fd4a4437026f562856d08996673a", size = 108884, upload-time = "2025-01-25T08:38:29.159Z" }, - { url = "https://files.pythonhosted.org/packages/bd/91/e59a66538a3364176f6c3f7620eee0ab195bfe26f89a95cbcc7a1fb04b28/mmh3-5.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57730067174a7f36fcd6ce012fe359bd5510fdaa5fe067bc94ed03e65dafb769", size = 106835, upload-time = "2025-01-25T08:38:33.04Z" }, - { url = "https://files.pythonhosted.org/packages/25/14/b85836e21ab90e5cddb85fe79c494ebd8f81d96a87a664c488cc9277668b/mmh3-5.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde80eb196d7fdc765a318604ded74a4378f02c5b46c17aa48a27d742edaded2", size = 93688, upload-time = "2025-01-25T08:38:34.987Z" }, - { url = "https://files.pythonhosted.org/packages/ac/aa/8bc964067df9262740c95e4cde2d19f149f2224f426654e14199a9e47df6/mmh3-5.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c8eddcb441abddeb419c16c56fd74b3e2df9e57f7aa2903221996718435c7a", size = 101569, upload-time = "2025-01-25T08:38:35.983Z" }, - { url = "https://files.pythonhosted.org/packages/70/b6/1fb163cbf919046a64717466c00edabebece3f95c013853fec76dbf2df92/mmh3-5.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:99e07e4acafbccc7a28c076a847fb060ffc1406036bc2005acb1b2af620e53c3", size = 98483, upload-time = "2025-01-25T08:38:38.198Z" }, - { url = "https://files.pythonhosted.org/packages/70/49/ba64c050dd646060f835f1db6b2cd60a6485f3b0ea04976e7a29ace7312e/mmh3-5.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e25ba5b530e9a7d65f41a08d48f4b3fedc1e89c26486361166a5544aa4cad33", size = 96496, upload-time = "2025-01-25T08:38:39.257Z" }, - { url = "https://files.pythonhosted.org/packages/9e/07/f2751d6a0b535bb865e1066e9c6b80852571ef8d61bce7eb44c18720fbfc/mmh3-5.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bb9bf7475b4d99156ce2f0cf277c061a17560c8c10199c910a680869a278ddc7", size = 105109, upload-time = "2025-01-25T08:38:40.395Z" }, - { url = "https://files.pythonhosted.org/packages/b7/02/30360a5a66f7abba44596d747cc1e6fb53136b168eaa335f63454ab7bb79/mmh3-5.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a1b0878dd281ea3003368ab53ff6f568e175f1b39f281df1da319e58a19c23a", size = 98231, upload-time = "2025-01-25T08:38:42.141Z" }, - { url = "https://files.pythonhosted.org/packages/8c/60/8526b0c750ff4d7ae1266e68b795f14b97758a1d9fcc19f6ecabf9c55656/mmh3-5.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:25f565093ac8b8aefe0f61f8f95c9a9d11dd69e6a9e9832ff0d293511bc36258", size = 97548, upload-time = "2025-01-25T08:38:43.402Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4c/26e1222aca65769280d5427a1ce5875ef4213449718c8f03958d0bf91070/mmh3-5.1.0-cp311-cp311-win32.whl", hash = "sha256:1e3554d8792387eac73c99c6eaea0b3f884e7130eb67986e11c403e4f9b6d372", size = 40810, upload-time = "2025-01-25T08:38:45.143Z" }, - { url = "https://files.pythonhosted.org/packages/98/d5/424ba95062d1212ea615dc8debc8d57983f2242d5e6b82e458b89a117a1e/mmh3-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad777a48197882492af50bf3098085424993ce850bdda406a358b6ab74be759", size = 41476, upload-time = "2025-01-25T08:38:46.029Z" }, - { url = "https://files.pythonhosted.org/packages/bd/08/0315ccaf087ba55bb19a6dd3b1e8acd491e74ce7f5f9c4aaa06a90d66441/mmh3-5.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f29dc4efd99bdd29fe85ed6c81915b17b2ef2cf853abf7213a48ac6fb3eaabe1", size = 38880, upload-time = "2025-01-25T08:38:47.035Z" }, - { url = "https://files.pythonhosted.org/packages/f4/47/e5f452bdf16028bfd2edb4e2e35d0441e4a4740f30e68ccd4cfd2fb2c57e/mmh3-5.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45712987367cb9235026e3cbf4334670522a97751abfd00b5bc8bfa022c3311d", size = 56152, upload-time = "2025-01-25T08:38:47.902Z" }, - { url = "https://files.pythonhosted.org/packages/60/38/2132d537dc7a7fdd8d2e98df90186c7fcdbd3f14f95502a24ba443c92245/mmh3-5.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b1020735eb35086ab24affbea59bb9082f7f6a0ad517cb89f0fc14f16cea4dae", size = 40564, upload-time = "2025-01-25T08:38:48.839Z" }, - { url = "https://files.pythonhosted.org/packages/c0/2a/c52cf000581bfb8d94794f58865658e7accf2fa2e90789269d4ae9560b16/mmh3-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:babf2a78ce5513d120c358722a2e3aa7762d6071cd10cede026f8b32452be322", size = 40104, upload-time = "2025-01-25T08:38:49.773Z" }, - { url = "https://files.pythonhosted.org/packages/83/33/30d163ce538c54fc98258db5621447e3ab208d133cece5d2577cf913e708/mmh3-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4f47f58cd5cbef968c84a7c1ddc192fef0a36b48b0b8a3cb67354531aa33b00", size = 102634, upload-time = "2025-01-25T08:38:51.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/5c/5a18acb6ecc6852be2d215c3d811aa61d7e425ab6596be940877355d7f3e/mmh3-5.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2044a601c113c981f2c1e14fa33adc9b826c9017034fe193e9eb49a6882dbb06", size = 108888, upload-time = "2025-01-25T08:38:52.542Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/11c556324c64a92aa12f28e221a727b6e082e426dc502e81f77056f6fc98/mmh3-5.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94d999c9f2eb2da44d7c2826d3fbffdbbbbcde8488d353fee7c848ecc42b968", size = 106968, upload-time = "2025-01-25T08:38:54.286Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/ca0c196a685aba7808a5c00246f17b988a9c4f55c594ee0a02c273e404f3/mmh3-5.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a015dcb24fa0c7a78f88e9419ac74f5001c1ed6a92e70fd1803f74afb26a4c83", size = 93771, upload-time = "2025-01-25T08:38:55.576Z" }, - { url = "https://files.pythonhosted.org/packages/b4/55/0927c33528710085ee77b808d85bbbafdb91a1db7c8eaa89cac16d6c513e/mmh3-5.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:457da019c491a2d20e2022c7d4ce723675e4c081d9efc3b4d8b9f28a5ea789bd", size = 101726, upload-time = "2025-01-25T08:38:56.654Z" }, - { url = "https://files.pythonhosted.org/packages/49/39/a92c60329fa470f41c18614a93c6cd88821412a12ee78c71c3f77e1cfc2d/mmh3-5.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71408579a570193a4ac9c77344d68ddefa440b00468a0b566dcc2ba282a9c559", size = 98523, upload-time = "2025-01-25T08:38:57.662Z" }, - { url = "https://files.pythonhosted.org/packages/81/90/26adb15345af8d9cf433ae1b6adcf12e0a4cad1e692de4fa9f8e8536c5ae/mmh3-5.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8b3a04bc214a6e16c81f02f855e285c6df274a2084787eeafaa45f2fbdef1b63", size = 96628, upload-time = "2025-01-25T08:38:59.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4d/340d1e340df972a13fd4ec84c787367f425371720a1044220869c82364e9/mmh3-5.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:832dae26a35514f6d3c1e267fa48e8de3c7b978afdafa0529c808ad72e13ada3", size = 105190, upload-time = "2025-01-25T08:39:00.483Z" }, - { url = "https://files.pythonhosted.org/packages/d3/7c/65047d1cccd3782d809936db446430fc7758bda9def5b0979887e08302a2/mmh3-5.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf658a61fc92ef8a48945ebb1076ef4ad74269e353fffcb642dfa0890b13673b", size = 98439, upload-time = "2025-01-25T08:39:01.484Z" }, - { url = "https://files.pythonhosted.org/packages/72/d2/3c259d43097c30f062050f7e861075099404e8886b5d4dd3cebf180d6e02/mmh3-5.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3313577453582b03383731b66447cdcdd28a68f78df28f10d275d7d19010c1df", size = 97780, upload-time = "2025-01-25T08:39:02.444Z" }, - { url = "https://files.pythonhosted.org/packages/29/29/831ea8d4abe96cdb3e28b79eab49cac7f04f9c6b6e36bfc686197ddba09d/mmh3-5.1.0-cp312-cp312-win32.whl", hash = "sha256:1d6508504c531ab86c4424b5a5ff07c1132d063863339cf92f6657ff7a580f76", size = 40835, upload-time = "2025-01-25T08:39:03.369Z" }, - { url = "https://files.pythonhosted.org/packages/12/dd/7cbc30153b73f08eeac43804c1dbc770538a01979b4094edbe1a4b8eb551/mmh3-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:aa75981fcdf3f21759d94f2c81b6a6e04a49dfbcdad88b152ba49b8e20544776", size = 41509, upload-time = "2025-01-25T08:39:04.284Z" }, - { url = "https://files.pythonhosted.org/packages/80/9d/627375bab4c90dd066093fc2c9a26b86f87e26d980dbf71667b44cbee3eb/mmh3-5.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4c1a76808dfea47f7407a0b07aaff9087447ef6280716fd0783409b3088bb3c", size = 38888, upload-time = "2025-01-25T08:39:05.174Z" }, - { url = "https://files.pythonhosted.org/packages/05/06/a098a42870db16c0a54a82c56a5bdc873de3165218cd5b3ca59dbc0d31a7/mmh3-5.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a523899ca29cfb8a5239618474a435f3d892b22004b91779fcb83504c0d5b8c", size = 56165, upload-time = "2025-01-25T08:39:06.887Z" }, - { url = "https://files.pythonhosted.org/packages/5a/65/eaada79a67fde1f43e1156d9630e2fb70655e1d3f4e8f33d7ffa31eeacfd/mmh3-5.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:17cef2c3a6ca2391ca7171a35ed574b5dab8398163129a3e3a4c05ab85a4ff40", size = 40569, upload-time = "2025-01-25T08:39:07.945Z" }, - { url = "https://files.pythonhosted.org/packages/36/7e/2b6c43ed48be583acd68e34d16f19209a9f210e4669421b0321e326d8554/mmh3-5.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52e12895b30110f3d89dae59a888683cc886ed0472dd2eca77497edef6161997", size = 40104, upload-time = "2025-01-25T08:39:09.598Z" }, - { url = "https://files.pythonhosted.org/packages/11/2b/1f9e962fdde8e41b0f43d22c8ba719588de8952f9376df7d73a434827590/mmh3-5.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d6719045cda75c3f40397fc24ab67b18e0cb8f69d3429ab4c39763c4c608dd", size = 102497, upload-time = "2025-01-25T08:39:10.512Z" }, - { url = "https://files.pythonhosted.org/packages/46/94/d6c5c3465387ba077cccdc028ab3eec0d86eed1eebe60dcf4d15294056be/mmh3-5.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d19fa07d303a91f8858982c37e6939834cb11893cb3ff20e6ee6fa2a7563826a", size = 108834, upload-time = "2025-01-25T08:39:11.568Z" }, - { url = "https://files.pythonhosted.org/packages/34/1e/92c212bb81796b69dddfd50a8a8f4b26ab0d38fdaf1d3e8628a67850543b/mmh3-5.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31b47a620d622fbde8ca1ca0435c5d25de0ac57ab507209245e918128e38e676", size = 106936, upload-time = "2025-01-25T08:39:12.638Z" }, - { url = "https://files.pythonhosted.org/packages/f4/41/f2f494bbff3aad5ffd2085506255049de76cde51ddac84058e32768acc79/mmh3-5.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f810647c22c179b6821079f7aa306d51953ac893587ee09cf1afb35adf87cb", size = 93709, upload-time = "2025-01-25T08:39:14.071Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a9/a2cc4a756d73d9edf4fb85c76e16fd56b0300f8120fd760c76b28f457730/mmh3-5.1.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6128b610b577eed1e89ac7177ab0c33d06ade2aba93f5c89306032306b5f1c6", size = 101623, upload-time = "2025-01-25T08:39:15.507Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6f/b9d735533b6a56b2d56333ff89be6a55ac08ba7ff33465feb131992e33eb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1e550a45d2ff87a1c11b42015107f1778c93f4c6f8e731bf1b8fa770321b8cc4", size = 98521, upload-time = "2025-01-25T08:39:16.77Z" }, - { url = "https://files.pythonhosted.org/packages/99/47/dff2b54fac0d421c1e6ecbd2d9c85b2d0e6f6ee0d10b115d9364116a511e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:785ae09276342f79fd8092633e2d52c0f7c44d56e8cfda8274ccc9b76612dba2", size = 96696, upload-time = "2025-01-25T08:39:17.805Z" }, - { url = "https://files.pythonhosted.org/packages/be/43/9e205310f47c43ddf1575bb3a1769c36688f30f1ac105e0f0c878a29d2cd/mmh3-5.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f4be3703a867ef976434afd3661a33884abe73ceb4ee436cac49d3b4c2aaa7b", size = 105234, upload-time = "2025-01-25T08:39:18.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/44/90b11fd2b67dcb513f5bfe9b476eb6ca2d5a221c79b49884dc859100905e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e513983830c4ff1f205ab97152a0050cf7164f1b4783d702256d39c637b9d107", size = 98449, upload-time = "2025-01-25T08:39:20.719Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/25c4b0c7b8e49836541059b28e034a4cccd0936202800d43a1cc48495ecb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9135c300535c828c0bae311b659f33a31c941572eae278568d1a953c4a57b59", size = 97796, upload-time = "2025-01-25T08:39:22.453Z" }, - { url = "https://files.pythonhosted.org/packages/23/fa/cbbb7fcd0e287a715f1cd28a10de94c0535bd94164e38b852abc18da28c6/mmh3-5.1.0-cp313-cp313-win32.whl", hash = "sha256:c65dbd12885a5598b70140d24de5839551af5a99b29f9804bb2484b29ef07692", size = 40828, upload-time = "2025-01-25T08:39:23.372Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/9fb90ef822f7b734955a63851907cf72f8a3f9d8eb3c5706bfa6772a2a77/mmh3-5.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:10db7765201fc65003fa998faa067417ef6283eb5f9bba8f323c48fd9c33e91f", size = 41504, upload-time = "2025-01-25T08:39:24.286Z" }, - { url = "https://files.pythonhosted.org/packages/16/71/4ad9a42f2772793a03cb698f0fc42499f04e6e8d2560ba2f7da0fb059a8e/mmh3-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b22fe2e54be81f6c07dcb36b96fa250fb72effe08aa52fbb83eade6e1e2d5fd7", size = 38890, upload-time = "2025-01-25T08:39:25.28Z" }, -] - -[[package]] -name = "monty" -version = "2025.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "ruamel-yaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/54/a1b2b4c9b16ebc5ea72cf22b1a6bb7ceaa79187fbf22a404c9677c8f90dd/monty-2025.3.3.tar.gz", hash = "sha256:16c1eb54b2372e765c2f3f14cff01cc76ab55c3cc12b27d49962fb8072310ae0", size = 85592, upload-time = "2025-03-03T21:12:44.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/df/b3a36544734be3ac0eacf11bcfb8609464dd07d8bad0dff6e46109c68002/monty-2025.3.3-py3-none-any.whl", hash = "sha256:5eadb6d748c007bc63c34eceb2d80faff18f3996121d261dbceeea22adc58775", size = 51925, upload-time = "2025-03-03T21:12:42.598Z" }, -] - [[package]] name = "more-itertools" version = "10.7.0" @@ -4092,7 +3834,7 @@ wheels = [ [[package]] name = "openai" -version = "2.8.1" +version = "1.109.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -4104,9 +3846,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" }, ] [[package]] @@ -5003,77 +4745,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] -[[package]] -name = "pycares" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/37/4d4f8ac929e98aad64781f37d9429e82ba65372fc89da0473cdbecdbbb03/pycares-4.9.0.tar.gz", hash = "sha256:8ee484ddb23dbec4d88d14ed5b6d592c1960d2e93c385d5e52b6fad564d82395", size = 655365, upload-time = "2025-06-13T00:37:49.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e8/9fce41cf9b077c68c3c867c1950a37d0b1f23608ae6f70ca823938d284ed/pycares-4.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b8bd9a3ee6e9bc990e1933dc7e7e2f44d4184f49a90fa444297ac12ab6c0c84", size = 145618, upload-time = "2025-06-13T00:36:26.728Z" }, - { url = "https://files.pythonhosted.org/packages/04/af/596a853dbdaab11ea8810bef01f68350d19b50cd3b5a8958b3d6ae91c93e/pycares-4.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:417a5c20861f35977240ad4961479a6778125bcac21eb2ad1c3aad47e2ff7fab", size = 140703, upload-time = "2025-06-13T00:36:28.332Z" }, - { url = "https://files.pythonhosted.org/packages/f4/de/39f4d3bff6fd8965f0d15b273e4e5ed3f05dc33836eeaad6a358ea519c31/pycares-4.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab290faa4ea53ce53e3ceea1b3a42822daffce2d260005533293a52525076750", size = 586814, upload-time = "2025-06-13T00:36:29.181Z" }, - { url = "https://files.pythonhosted.org/packages/4c/e0/3b02a2866b3c43f02e64e6784b38e7d923c0350670574c4f559829fe6717/pycares-4.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1df81193084c9717734e4615e8c5074b9852478c9007d1a8bb242f7f580e67", size = 627237, upload-time = "2025-06-13T00:36:31.01Z" }, - { url = "https://files.pythonhosted.org/packages/40/93/ef972f9691c2860fc9f64eb60cf1239286dea95e7eadac6bc13906d6dd06/pycares-4.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20c7a6af0c2ccd17cc5a70d76e299a90e7ebd6c4d8a3d7fff5ae533339f61431", size = 665198, upload-time = "2025-06-13T00:36:32.343Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8a/e751d9574ca6e879b7b2ca75e882aa59228e4e50ed11d844bbb546c0b347/pycares-4.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:370f41442a5b034aebdb2719b04ee04d3e805454a20d3f64f688c1c49f9137c3", size = 647509, upload-time = "2025-06-13T00:36:33.504Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1c/e2af8de7f088962fd1436cb27e8602b320f5a6568d8b1ac45163f23d455a/pycares-4.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:340e4a3bbfd14d73c01ec0793a321b8a4a93f64c508225883291078b7ee17ac8", size = 627953, upload-time = "2025-06-13T00:36:34.571Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d4/556d8f1ef10a65e2f22f98b07dc4fc5d7a3148354919316badb843083dac/pycares-4.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f0ec94785856ea4f5556aa18f4c027361ba4b26cb36c4ad97d2105ef4eec68ba", size = 626222, upload-time = "2025-06-13T00:36:36.037Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/d3eaa380e172953be60f6c57bf1d0c482a7606de8eb95802df038528a0ca/pycares-4.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6b7e23a4a9e2039b5d67dfa0499d2d5f114667dc13fb5d7d03eed230c7ac4f", size = 596007, upload-time = "2025-06-13T00:36:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/e5/6c/3505d253e2f1c2ffc2d4459393fc0cec44ae99564a683892f696c0ec953b/pycares-4.9.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:490c978b0be9d35a253a5e31dd598f6d66b453625f0eb7dc2d81b22b8c3bb3f4", size = 671916, upload-time = "2025-06-13T00:36:38.142Z" }, - { url = "https://files.pythonhosted.org/packages/96/33/fad5aed6c87574557fb2bc40ad212864868224ffb656698d23ca2faab720/pycares-4.9.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e433faaf07f44e44f1a1b839fee847480fe3db9431509dafc9f16d618d491d0f", size = 655593, upload-time = "2025-06-13T00:36:39.226Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/6a7951a33ff03188907627528e466984e090c8c006fd46d29151a7842d7b/pycares-4.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf6d8851a06b79d10089962c9dadcb34dad00bf027af000f7102297a54aaff2e", size = 630867, upload-time = "2025-06-13T00:36:40.261Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/7fad8367b93e8c27572994946079254e521cf297a425bf6e76ed721a2e3a/pycares-4.9.0-cp310-cp310-win32.whl", hash = "sha256:4f803e7d66ac7d8342998b8b07393788991353a46b05bbaad0b253d6f3484ea8", size = 118454, upload-time = "2025-06-13T00:36:41.21Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/09a86285b86c443300180fbf63e4080face96412ab892ef5ac1a7ee86966/pycares-4.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e17bd32267e3870855de3baed7d0efa6337344d68f44853fd9195c919f39400", size = 143739, upload-time = "2025-06-13T00:36:42.379Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9d/e5c621a014bc9d43430ce96bb4b791db34df51c020db2fc16ce0fe29c999/pycares-4.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:6b74f75d8e430f9bb11a1cc99b2e328eed74b17d8d4b476de09126f38d419eb9", size = 115638, upload-time = "2025-06-13T00:36:43.27Z" }, - { url = "https://files.pythonhosted.org/packages/3a/34/6ce1d521c41cb99ac3ec550f8590e122a749d9999cb7020259a93b987b6f/pycares-4.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16a97ee83ec60d35c7f716f117719932c27d428b1bb56b242ba1c4aa55521747", size = 145616, upload-time = "2025-06-13T00:36:44.313Z" }, - { url = "https://files.pythonhosted.org/packages/fe/fd/0c533533c8805ca85dfa6a5f5a91cc776bb817b7e2cf05dffefd3c98ab55/pycares-4.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:78748521423a211ce699a50c27cc5c19e98b7db610ccea98daad652ace373990", size = 140703, upload-time = "2025-06-13T00:36:45.643Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/0e9082d27ba1c3c5795c30d8edfb8dd57fe1f8e18cd7d70673325b8be693/pycares-4.9.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8818b2c7a57d9d6d41e8b64d9ff87992b8ea2522fc0799686725228bc3cff6c5", size = 586801, upload-time = "2025-06-13T00:36:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b7/1c137a3df0d992cbc292116ed4ad369d39fdfc504fe8c63427e42147f8fd/pycares-4.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96df8990f16013ca5194d6ece19dddb4ef9cd7c3efaab9f196ec3ccd44b40f8d", size = 627231, upload-time = "2025-06-13T00:36:47.898Z" }, - { url = "https://files.pythonhosted.org/packages/86/24/9f27732c0b6ebb36eb1d01e9bccff7f2540ae2c9e2a06987e762372132f6/pycares-4.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61af86fd58b8326e723b0d20fb96b56acaec2261c3a7c9a1c29d0a79659d613a", size = 665185, upload-time = "2025-06-13T00:36:48.884Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f5/474438dd01f5deaddb4107d2106fc9e35c53b2fdfa48da85532cc9295f25/pycares-4.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec72edb276bda559813cc807bc47b423d409ffab2402417a5381077e9c2c6be1", size = 647498, upload-time = "2025-06-13T00:36:50.245Z" }, - { url = "https://files.pythonhosted.org/packages/26/ae/9504d5b773e6d9c7da9ed324e3074d3b842a5db265994dd819f5469f367c/pycares-4.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832fb122c7376c76cab62f8862fa5e398b9575fb7c9ff6bc9811086441ee64ca", size = 627953, upload-time = "2025-06-13T00:36:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/54/74/30eaa7168fec4c6df906201f89cfaa47a4319ed5df4fbcaee5efbbfb1c3c/pycares-4.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdcfaef24f771a471671470ccfd676c0366ab6b0616fd8217b8f356c40a02b83", size = 626202, upload-time = "2025-06-13T00:36:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/16/25/97ce60be2e71a12b0fdc045144b3aa800ef6acef76727407344d12fe2c62/pycares-4.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:52cb056d06ff55d78a8665b97ae948abaaba2ca200ca59b10346d4526bce1e7d", size = 595945, upload-time = "2025-06-13T00:36:54.214Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a5/1882f565e0ffb1129d3b976aac1fdc3542f5bfa6524151abc95c473dc607/pycares-4.9.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:54985ed3f2e8a87315269f24cb73441622857a7830adfc3a27c675a94c3261c1", size = 671998, upload-time = "2025-06-13T00:36:55.533Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4e/721dae2c2bdeea3f7038dbe1c77c2405540ec3c4123c9468471b180fcabe/pycares-4.9.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:08048e223615d4aef3dac81fe0ea18fb18d6fc97881f1eb5be95bb1379969b8d", size = 655551, upload-time = "2025-06-13T00:36:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/30/24/b4c2b6a4708dac5f99592f905bc4e20c183ff0c3c9eedc7eaf8468eeae40/pycares-4.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc60037421ce05a409484287b2cd428e1363cca73c999b5f119936bb8f255208", size = 630862, upload-time = "2025-06-13T00:36:57.936Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a4/63c3c4e3fdfa58d8e83f2976e4a0715476a1a77bb9032b4d050c689d575e/pycares-4.9.0-cp311-cp311-win32.whl", hash = "sha256:62b86895b60cfb91befb3086caa0792b53f949231c6c0c3053c7dfee3f1386ab", size = 118455, upload-time = "2025-06-13T00:36:58.9Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/ccd375a8ea6c4e880aa191bbaf63b133e27441725a05f301c1f90eab76cd/pycares-4.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7046b3c80954beaabf2db52b09c3d6fe85f6c4646af973e61be79d1c51589932", size = 143742, upload-time = "2025-06-13T00:36:59.807Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/3fc962b2b8b38d5e916f42ba20fa2a147ddc181d3bac0665f7f6b8da9854/pycares-4.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:fcbda3fdf44e94d3962ca74e6ba3dc18c0d7029106f030d61c04c0876f319403", size = 115635, upload-time = "2025-06-13T00:37:00.634Z" }, - { url = "https://files.pythonhosted.org/packages/75/96/9b147e40873ae3253a06e3080674c7fdd62ee3e7cbf7b47f43cd213db638/pycares-4.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d68ca2da1001aeccdc81c4a2fb1f1f6cfdafd3d00e44e7c1ed93e3e05437f666", size = 145590, upload-time = "2025-06-13T00:37:01.463Z" }, - { url = "https://files.pythonhosted.org/packages/02/df/94928bb701ee6b84a8a91db4da4b1dd008ece6f62ef58c149a3b410b5a11/pycares-4.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4f0c8fa5a384d79551a27eafa39eed29529e66ba8fa795ee432ab88d050432a3", size = 140732, upload-time = "2025-06-13T00:37:02.415Z" }, - { url = "https://files.pythonhosted.org/packages/1c/79/cd7b25ea35e15ae3dd10a1722f228c12abb9d0540a5056c0ce9ed639e29c/pycares-4.9.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb8c428cf3b9c6ff9c641ba50ab6357b4480cd737498733e6169b0ac8a1a89b", size = 587481, upload-time = "2025-06-13T00:37:03.315Z" }, - { url = "https://files.pythonhosted.org/packages/36/2c/fec91d98028a6c9a60b721238f787b95280c2e760716450834ba28ffdbe9/pycares-4.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6845bd4a43abf6dab7fedbf024ef458ac3750a25b25076ea9913e5ac5fec4548", size = 628295, upload-time = "2025-06-13T00:37:04.356Z" }, - { url = "https://files.pythonhosted.org/packages/26/27/aee4cfb25f285fe7d9c26be55a39f1605a63f947fcd0e65ae65cbfcfda3d/pycares-4.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e28f4acc3b97e46610cf164665ebf914f709daea6ced0ca4358ce55bc1c3d6b", size = 665519, upload-time = "2025-06-13T00:37:05.774Z" }, - { url = "https://files.pythonhosted.org/packages/fa/61/97352377482512b379f1898fe2de038fa6d6774343423d1c03022a033f6b/pycares-4.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9464a39861840ce35a79352c34d653a9db44f9333af7c9feddb97998d3e00c07", size = 648274, upload-time = "2025-06-13T00:37:06.981Z" }, - { url = "https://files.pythonhosted.org/packages/40/bf/38779a4097d2d840b1f3f9ffa417d5b546dcba0702c8c73909bcb53d4965/pycares-4.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0611c1bd46d1fc6bdd9305b8850eb84c77df485769f72c574ed7b8389dfbee2", size = 629257, upload-time = "2025-06-13T00:37:08.025Z" }, - { url = "https://files.pythonhosted.org/packages/0a/83/01d18e21a8a70996959c9ce86bca863eebe76fa7bb54ff29b6b7346b898e/pycares-4.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4fb5a38a51d03b75ac4320357e632c2e72e03fdeb13263ee333a40621415fdc", size = 621130, upload-time = "2025-06-13T00:37:09.016Z" }, - { url = "https://files.pythonhosted.org/packages/54/08/8755dbe628e0335231e975abc47114b444b79eace488446dffd7667f3b22/pycares-4.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df5edae05fb3e1370ab7639e67e8891fdaa9026cb10f05dbd57893713f7a9cfe", size = 593493, upload-time = "2025-06-13T00:37:10.05Z" }, - { url = "https://files.pythonhosted.org/packages/63/21/50db3e05eded5442b7c8426d8b956bdc5b1c210f14c8a22b6d01e6b81700/pycares-4.9.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:397123ea53d261007bb0aa7e767ef238778f45026db40bed8196436da2cc73de", size = 669045, upload-time = "2025-06-13T00:37:11.138Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fc/139fc9a9ba32fd6052e50cbcae00bd7a2be60bdbf4555b5aae9a113f6f8c/pycares-4.9.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bb0d874d0b131b29894fd8a0f842be91ac21d50f90ec04cff4bb3f598464b523", size = 652084, upload-time = "2025-06-13T00:37:12.125Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bc/513a6c0d73dddaf3b17be9aee865b60b608cc6acb80f7da1a33a85608e85/pycares-4.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:497cc03a61ec1585eb17d2cb086a29a6a67d24babf1e9be519b47222916a3b06", size = 628361, upload-time = "2025-06-13T00:37:13.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/ea/852ecd06739e0c54d2dea1935ff8bebbd7ca598fb98293700213b137e955/pycares-4.9.0-cp312-cp312-win32.whl", hash = "sha256:b46e46313fdb5e82da15478652aac0fd15e1c9f33e08153bad845aa4007d6f84", size = 118511, upload-time = "2025-06-13T00:37:14.495Z" }, - { url = "https://files.pythonhosted.org/packages/ff/34/8cb69f8ac128d3a8635995c4ef9192dbe75a88e22794d9f466813229d782/pycares-4.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:12547a06445777091605a7581da15a0da158058beb8a05a3ebbf7301fd1f58d4", size = 143744, upload-time = "2025-06-13T00:37:15.369Z" }, - { url = "https://files.pythonhosted.org/packages/12/e8/510cc7100b452443bb00785e0489b55694e61668cc14bb35801797218be7/pycares-4.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:f1e10bf1e8eb80b08e5c828627dba1ebc4acd54803bd0a27d92b9063b6aa99d8", size = 115641, upload-time = "2025-06-13T00:37:16.236Z" }, - { url = "https://files.pythonhosted.org/packages/10/da/e0240d156c6089bf2b38afd01600fe9db8b1dd6e53fb776f1dca020b1124/pycares-4.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:574d815112a95ab09d75d0a9dc7dea737c06985e3125cf31c32ba6a3ed6ca006", size = 145589, upload-time = "2025-06-13T00:37:17.154Z" }, - { url = "https://files.pythonhosted.org/packages/27/c5/1d4abd1a33b7fbd4dc0e854fcd6c76c4236bdfe1359dafb0a8349694462d/pycares-4.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50e5ab06361d59625a27a7ad93d27e067dc7c9f6aa529a07d691eb17f3b43605", size = 140730, upload-time = "2025-06-13T00:37:18.088Z" }, - { url = "https://files.pythonhosted.org/packages/24/4d/3ff037cd7fb7a6d9f1bf4289b96ff2d6ac59d098f02bbf3b18cb0a0ab576/pycares-4.9.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:785f5fd11ff40237d9bc8afa441551bb449e2812c74334d1d10859569e07515c", size = 587384, upload-time = "2025-06-13T00:37:19.047Z" }, - { url = "https://files.pythonhosted.org/packages/66/92/be8f527017769148687e45a4e5afd8d849aee2b145cda59003ad5a531aaf/pycares-4.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e194a500e403eba89b91fb863c917495c5b3dfcd1ce0ee8dc3a6f99a1360e2fc", size = 628273, upload-time = "2025-06-13T00:37:20.304Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8d/e88cfdd08f7065ae52817b930834964320d0e43955f6ac68d2ab35728912/pycares-4.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:112dd49cdec4e6150a8d95b197e8b6b7b4468a3170b30738ed9b248cb2240c04", size = 665481, upload-time = "2025-06-13T00:37:21.727Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9d/a2661f9c8e1e7fa842586d7b24710e78f068d26f768eea7a7437c249a2f6/pycares-4.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94aa3c2f3eb0aa69160137134775501f06c901188e722aac63d2a210d4084f99", size = 648157, upload-time = "2025-06-13T00:37:22.801Z" }, - { url = "https://files.pythonhosted.org/packages/43/b9/d04ea1de2a7d4e8a00b2b00a0ee94d7b0434f00eb55f5941ffa287c1dab2/pycares-4.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b510d71255cf5a92ccc2643a553548fcb0623d6ed11c8c633b421d99d7fa4167", size = 629244, upload-time = "2025-06-13T00:37:23.868Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c8/7f81ccdd856ddc383d3f82708b4f4022761640f3baec6d233549960348b8/pycares-4.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5c6aa30b1492b8130f7832bf95178642c710ce6b7ba610c2b17377f77177e3cd", size = 621120, upload-time = "2025-06-13T00:37:25.164Z" }, - { url = "https://files.pythonhosted.org/packages/fd/96/9386654a244caafd77748e626da487f1a56f831e3db5ef1337410be3e5f6/pycares-4.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5767988e044faffe2aff6a76aa08df99a8b6ef2641be8b00ea16334ce5dea93", size = 593493, upload-time = "2025-06-13T00:37:26.198Z" }, - { url = "https://files.pythonhosted.org/packages/76/bd/73286f329d03fef071e8517076dc62487e4478a3c85c4c59d652e6a663e5/pycares-4.9.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b9928a942820a82daa3207509eaba9e0fa9660756ac56667ec2e062815331fcb", size = 669086, upload-time = "2025-06-13T00:37:27.278Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2a/0f623426225828f2793c3f86463ef72f6ecf6df12fe240a4e68435e8212f/pycares-4.9.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:556c854174da76d544714cdfab10745ed5d4b99eec5899f7b13988cd26ff4763", size = 652103, upload-time = "2025-06-13T00:37:28.361Z" }, - { url = "https://files.pythonhosted.org/packages/04/d8/7db6eee011f414f21e3d53a0ad81593baa87a332403d781c2f86d3eef315/pycares-4.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d42e2202ca9aa9a0a9a6e43a4a4408bbe0311aaa44800fa27b8fd7f82b20152a", size = 628373, upload-time = "2025-06-13T00:37:29.797Z" }, - { url = "https://files.pythonhosted.org/packages/72/a4/1a9b96678afb4f31651885129fbfa2cd44e78a438fd545c7b8d317a1f381/pycares-4.9.0-cp313-cp313-win32.whl", hash = "sha256:cce8ef72c9ed4982c84114e6148a4e42e989d745de7862a0ad8b3f1cdc05def2", size = 118511, upload-time = "2025-06-13T00:37:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/79/e4/6724c71a08a91f2685ca60ca35d7950c187a2d79a776461130a6cb5b0d5e/pycares-4.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:318cdf24f826f1d2f0c5a988730bd597e1683296628c8f1be1a5b96643c284fe", size = 143746, upload-time = "2025-06-13T00:37:32.015Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f8/b4d4bf71ae92727a0b3a9b9092c2e722833c1ca50ebd0414824843cb84fd/pycares-4.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:faa9de8e647ed06757a2c117b70a7645a755561def814da6aca0d766cf71a402", size = 115646, upload-time = "2025-06-13T00:37:33.251Z" }, -] - [[package]] name = "pycparser" version = "2.22" @@ -5389,77 +5060,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/e8/11644fe823e05c583b330e9fb81e3e8fc5d079036512a8300fc157be349d/pykakasi-2.3.0-py3-none-any.whl", hash = "sha256:26d21b090048ff45c6a4d8e962426b7951767216008ec30358e8a9d74af77f29", size = 2395003, upload-time = "2024-06-24T04:57:18.101Z" }, ] -[[package]] -name = "pymongo" -version = "4.15.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/a0/5c324fe6735b2bc189779ff46e981a59d495a74594f45542159125d77256/pymongo-4.15.5.tar.gz", hash = "sha256:3a8d6bf2610abe0c97c567cf98bf5bba3e90ccc93cc03c9dde75fa11e4267b42", size = 2471889, upload-time = "2025-12-02T18:44:30.992Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/e4/d80061be4e53125597dd2916171c87986043b190e50c1834fff455e71d42/pymongo-4.15.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a01a2054d50b50c121c720739a2216d855c48726b0002894de9b991cdd68a2a5", size = 811318, upload-time = "2025-12-02T18:42:12.09Z" }, - { url = "https://files.pythonhosted.org/packages/fb/b3/c499fe0814e4d3a84fa3ff5df5133bf847529d8b5a051e6108b5a25b75c7/pymongo-4.15.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e57968139d81367117ed7b75d921445a575d4d7e61536f5e860475df92ac0a9", size = 811676, upload-time = "2025-12-02T18:42:14.396Z" }, - { url = "https://files.pythonhosted.org/packages/62/71/8e21a8a680546b3a90afbb878a16fe2a7cb0f7d9652aa675c172e57856a1/pymongo-4.15.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:266aa37e3673e5dcfdd359a81d27131fc133e49cf8e5d9f9f27a5845fac2cd1f", size = 1185485, upload-time = "2025-12-02T18:42:16.147Z" }, - { url = "https://files.pythonhosted.org/packages/03/56/bdc292a7b01aa2aba806883dbcacc3be837d65425453aa2bc27954ba5a55/pymongo-4.15.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2883da6bd0545cc2f12672f6a609b33d48e099a220872ca2bf9bf29fe96a32c3", size = 1203866, upload-time = "2025-12-02T18:42:18.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e2/12bebc7e93a81c2f804ffcc94997f61f0e2cd2c11bf0f01da8e0e1425e5c/pymongo-4.15.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2fc32b354a608ec748d89bbe236b74b967890667eea1af54e92dfd8fbf26df52", size = 1242550, upload-time = "2025-12-02T18:42:19.898Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ac/c48f6f59a660ec44052ee448dea1c71da85cfaa4a0c17c726d4ee2db7716/pymongo-4.15.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c006cbaa4b40d296dd2bb8828976866c876ead4c39032b761dcf26f1ba56fde", size = 1232844, upload-time = "2025-12-02T18:42:21.709Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/6368befca7a2f3b51460755a373f78b72003aeee95e8e138cbd479c307f4/pymongo-4.15.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce21e3dc5939b83d03f871090d83ac29fef055bd057f8d3074b6cad10f86b04c", size = 1200192, upload-time = "2025-12-02T18:42:23.605Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/bc810a017ebb20e6e301fa8c5b21c5e53691fdde2cfd39bd9c450e957b14/pymongo-4.15.5-cp310-cp310-win32.whl", hash = "sha256:1b545dcf66a9f06e9b501bfb0438e1eb9af67336e8a5cf36c4bc0a5d3fbe7a37", size = 798338, upload-time = "2025-12-02T18:42:25.438Z" }, - { url = "https://files.pythonhosted.org/packages/46/17/3be0b476a6bfb3a51bf1750323b5eddf883dddb6482ccb8dbcab2c6c48ad/pymongo-4.15.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ecc544f515f828f05d3c56cd98063ba3ef8b75f534c63de43306d59f1e93fcd", size = 808153, upload-time = "2025-12-02T18:42:26.889Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0a/39f9daf16d695abd58987bb5e2c164b5a64e42b8d53d3c43bc06e4aa7dfc/pymongo-4.15.5-cp310-cp310-win_arm64.whl", hash = "sha256:1151968ab90db146f0591b6c7db27ce4f73c7ffa0bbddc1d7fb7cb14c9f0b967", size = 800943, upload-time = "2025-12-02T18:42:28.668Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/e43387c2ed78a60ad917c45f4d4de4f6992929d63fe15af4c2e624f093a9/pymongo-4.15.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:57157a4b936e28e2fbe7017b2f6a751da5e284675cab371f2c596d4e0e4f58f3", size = 865894, upload-time = "2025-12-02T18:42:30.496Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8c/f2c9c55adb9709a4b2244d8d8d9ec05e4abb274e03fe8388b58a34ae08b0/pymongo-4.15.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2a34a7391f4cc54fc584e49db6f7c3929221a9da08b3af2d2689884a5943843", size = 866235, upload-time = "2025-12-02T18:42:31.862Z" }, - { url = "https://files.pythonhosted.org/packages/5e/aa/bdf3553d7309b0ebc0c6edc23f43829b1758431f2f2f7385d2427b20563b/pymongo-4.15.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:be040c8cdaf9c2d5ae9ab60a67ecab453ec19d9ccd457a678053fdceab5ee4c8", size = 1429787, upload-time = "2025-12-02T18:42:33.829Z" }, - { url = "https://files.pythonhosted.org/packages/b3/55/80a8eefc88f578fde56489e5278ba5caa5ee9b6f285959ed2b98b44e2133/pymongo-4.15.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:defe93944526b1774265c16acf014689cb1b0b18eb84a7b370083b214f9e18cd", size = 1456747, upload-time = "2025-12-02T18:42:35.805Z" }, - { url = "https://files.pythonhosted.org/packages/1d/54/6a7ec290c7ab22aab117ab60e7375882ec5af7433eaf077f86e187a3a9e8/pymongo-4.15.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:816e66116f0ef868eff0463a8b28774af8b547466dbad30c8e82bf0325041848", size = 1514670, upload-time = "2025-12-02T18:42:37.737Z" }, - { url = "https://files.pythonhosted.org/packages/65/8a/5822aa20b274ee8a8821bf0284f131e7fc555b0758c3f2a82c51ae73a3c6/pymongo-4.15.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66c7b332532e0f021d784d04488dbf7ed39b7e7d6d5505e282ec8e9cf1025791", size = 1500711, upload-time = "2025-12-02T18:42:39.61Z" }, - { url = "https://files.pythonhosted.org/packages/32/ca/63984e32b4d745a25445c9da1159dfe4568a03375f32bb1a9e009dccb023/pymongo-4.15.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:acc46a9e47efad8c5229e644a3774169013a46ee28ac72d1fa4edd67c0b7ee9b", size = 1452021, upload-time = "2025-12-02T18:42:41.323Z" }, - { url = "https://files.pythonhosted.org/packages/f1/23/0d6988f3fdfcacae2ac8d7b76eb24f80ebee9eb607c53bcebfad75b7fd85/pymongo-4.15.5-cp311-cp311-win32.whl", hash = "sha256:b9836c28ba350d8182a51f32ef9bb29f0c40e82ba1dfb9e4371cd4d94338a55d", size = 844483, upload-time = "2025-12-02T18:42:42.814Z" }, - { url = "https://files.pythonhosted.org/packages/8e/04/dedff8a5a9539e5b6128d8d2458b9c0c83ebd38b43389620a0d97223f114/pymongo-4.15.5-cp311-cp311-win_amd64.whl", hash = "sha256:3a45876c5c2ab44e2a249fb542eba2a026f60d6ab04c7ef3924eae338d9de790", size = 859194, upload-time = "2025-12-02T18:42:45.025Z" }, - { url = "https://files.pythonhosted.org/packages/67/e5/fb6f49bceffe183e66831c2eebd2ea14bd65e2816aeaf8e2fc018fd8c344/pymongo-4.15.5-cp311-cp311-win_arm64.whl", hash = "sha256:e4a48fc5c712b3db85c9987cfa7fde0366b7930018de262919afd9e52cfbc375", size = 848377, upload-time = "2025-12-02T18:42:47.19Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4e/8f9fcb2dc9eab1fb0ed02da31e7f4847831d9c0ef08854a296588b97e8ed/pymongo-4.15.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c33477af1a50d1b4d86555e098fc2cf5992d839ad538dea0c00a8682162b7a75", size = 920955, upload-time = "2025-12-02T18:42:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b4/c0808bed1f82b3008909b9562615461e59c3b66f8977e502ea87c88b08a4/pymongo-4.15.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e6b30defa4a52d3698cd84d608963a8932f7e9b6ec5130087e7082552ac685e5", size = 920690, upload-time = "2025-12-02T18:42:50.832Z" }, - { url = "https://files.pythonhosted.org/packages/12/f3/feea83150c6a0cd3b44d5f705b1c74bff298a36f82d665f597bf89d42b3f/pymongo-4.15.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:45fec063f5672e6173bcb09b492431e3641cc74399c2b996fcb995881c2cac61", size = 1690351, upload-time = "2025-12-02T18:42:53.402Z" }, - { url = "https://files.pythonhosted.org/packages/d7/4e/15924d33d8d429e4c41666090017c6ac5e7ccc4ce5e435a2df09e45220a8/pymongo-4.15.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c6813110c0d9fde18674b7262f47a2270ae46c0ddd05711e6770caa3c9a3fb", size = 1726089, upload-time = "2025-12-02T18:42:56.187Z" }, - { url = "https://files.pythonhosted.org/packages/a5/49/650ff29dc5f9cf090dfbd6fb248c56d8a10d268b6f46b10fb02fbda3c762/pymongo-4.15.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8ec48d1db9f44c737b13be4299a1782d5fde3e75423acbbbe927cb37ebbe87d", size = 1800637, upload-time = "2025-12-02T18:42:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/f34661ade670ee42331543f4aa229569ac7ef45907ecda41b777137b9f40/pymongo-4.15.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f410694fdd76631ead7df6544cdeadaf2407179196c3642fced8e48bb21d0a6", size = 1785480, upload-time = "2025-12-02T18:43:00.626Z" }, - { url = "https://files.pythonhosted.org/packages/10/b6/378bb26937f6b366754484145826aca2d2361ac05b0bacd45a35876abcef/pymongo-4.15.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8c46765d6ac5727a899190aacdeec7a57f8c93346124ddd7e12633b573e2e65", size = 1718548, upload-time = "2025-12-02T18:43:02.32Z" }, - { url = "https://files.pythonhosted.org/packages/58/79/31b8afba36f794a049633e105e45c30afaa0e1c0bab48332d999e87d4860/pymongo-4.15.5-cp312-cp312-win32.whl", hash = "sha256:647118a58dca7d3547714fc0b383aebf81f5852f4173dfd77dd34e80eea9d29b", size = 891319, upload-time = "2025-12-02T18:43:04.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/31/a7e6d8c5657d922872ac75ab1c0a1335bfb533d2b4dad082d5d04089abbb/pymongo-4.15.5-cp312-cp312-win_amd64.whl", hash = "sha256:099d3e2dddfc75760c6a8fadfb99c1e88824a99c2c204a829601241dff9da049", size = 910919, upload-time = "2025-12-02T18:43:06.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b4/286c12fa955ae0597cd4c763d87c986e7ade681d4b11a81766f62f079c79/pymongo-4.15.5-cp312-cp312-win_arm64.whl", hash = "sha256:649cb906882c4058f467f334fb277083998ba5672ffec6a95d6700db577fd31a", size = 896357, upload-time = "2025-12-02T18:43:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/9b/92/e70db1a53bc0bb5defe755dee66b5dfbe5e514882183ffb696d6e1d38aa2/pymongo-4.15.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b736226f9001bbbd02f822acb9b9b6d28319f362f057672dfae2851f7da6125", size = 975324, upload-time = "2025-12-02T18:43:11.074Z" }, - { url = "https://files.pythonhosted.org/packages/a4/90/dd78c059a031b942fa36d71796e94a0739ea9fb4251fcd971e9579192611/pymongo-4.15.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60ea9f07fbbcc7c88f922082eb27436dce6756730fdef76a3a9b4c972d0a57a3", size = 975129, upload-time = "2025-12-02T18:43:13.345Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/87cf1bb75ef296456912eb7c6d51ebe7a36dbbe9bee0b8a9cd02a62a8a6e/pymongo-4.15.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20af63218ae42870eaee31fb8cc4ce9e3af7f04ea02fc98ad751fb7a9c8d7be3", size = 1950973, upload-time = "2025-12-02T18:43:15.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/68/dfa507c8e5cebee4e305825b436c34f5b9ba34488a224b7e112a03dbc01e/pymongo-4.15.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20d9c11625392f1f8dec7688de5ce344e110ca695344efa313ae4839f13bd017", size = 1995259, upload-time = "2025-12-02T18:43:16.869Z" }, - { url = "https://files.pythonhosted.org/packages/85/9d/832578e5ed7f682a09441bbc0881ffd506b843396ef4b34ec53bd38b2fb2/pymongo-4.15.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1202b3e5357b161acb7b7cc98e730288a5c15544e5ef7254b33931cb9a27c36e", size = 2086591, upload-time = "2025-12-02T18:43:19.559Z" }, - { url = "https://files.pythonhosted.org/packages/0a/99/ca8342a0cefd2bb1392187ef8fe01432855e3b5cd1e640495246bcd65542/pymongo-4.15.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63af710e9700dbf91abccf119c5f5533b9830286d29edb073803d3b252862c0d", size = 2070200, upload-time = "2025-12-02T18:43:21.214Z" }, - { url = "https://files.pythonhosted.org/packages/3f/7d/f4a9c1fceaaf71524ff9ff964cece0315dcc93df4999a49f064564875bff/pymongo-4.15.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22eeb86861cf7b8ee6886361d52abb88e3cd96c6f6d102e45e2604fc6e9e316", size = 1985263, upload-time = "2025-12-02T18:43:23.415Z" }, - { url = "https://files.pythonhosted.org/packages/d8/15/f942535bcc6e22d3c26c7e730daf296ffe69d8ce474c430ea7e551f8cf33/pymongo-4.15.5-cp313-cp313-win32.whl", hash = "sha256:aad6efe82b085bf77cec2a047ded2c810e93eced3ccf1a8e3faec3317df3cd52", size = 938143, upload-time = "2025-12-02T18:43:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/02/2a/c92a6927d676dd376d1ae05c680139c5cad068b22e5f0c8cb61014448894/pymongo-4.15.5-cp313-cp313-win_amd64.whl", hash = "sha256:ccc801f6d71ebee2ec2fb3acc64b218fa7cdb7f57933b2f8eee15396b662a0a0", size = 962603, upload-time = "2025-12-02T18:43:27.816Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f0/cdf78e9ed9c26fb36b8d75561ebf3c7fe206ff1c3de2e1b609fccdf3a55b/pymongo-4.15.5-cp313-cp313-win_arm64.whl", hash = "sha256:f043abdf20845bf29a554e95e4fe18d7d7a463095d6a1547699a12f80da91e02", size = 944308, upload-time = "2025-12-02T18:43:29.371Z" }, - { url = "https://files.pythonhosted.org/packages/03/0c/49713e0f8f41110e8b2bcce7c88570b158cf43dd53a0d01d4e1c772c7ede/pymongo-4.15.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba0e75a390334221744e2666fd2d4c82419b580c9bc8d6e0d2d61459d263f3af", size = 1029996, upload-time = "2025-12-02T18:43:31.58Z" }, - { url = "https://files.pythonhosted.org/packages/23/de/1df5d7b49647e9e4511054f750c1109cb8e160763b286b96879917170618/pymongo-4.15.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:853ec7da97642eabaf94d3de4453a86365729327d920af167bf14b2e87b24dce", size = 1029612, upload-time = "2025-12-02T18:43:33.69Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/3a051228e5beb0b421d725bb2ab5207a260c718d9b5be5b85cfe963733e3/pymongo-4.15.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7631304106487480ebbd8acbe44ff1e69d1fdc27e83d9753dc1fd227cea10761", size = 2211814, upload-time = "2025-12-02T18:43:35.769Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b3/989531a056c4388ef18245d1a6d6b3ec5c538666b000764286119efbf194/pymongo-4.15.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50505181365eba5d4d35c462870b3614c8eddd0b2407c89377c1a59380640dd9", size = 2264629, upload-time = "2025-12-02T18:43:37.479Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5f/8b3339fec44d0ba6d9388a19340fb1534c85ab6aa9fd8fb9c1af146bb72a/pymongo-4.15.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b75ec7006471299a571d6db1c5609ea4aa9c847a701e9b2953a8ede705d82db", size = 2371823, upload-time = "2025-12-02T18:43:39.866Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7f/706bf45cf12990b6cb73e6290b048944a51592de7a597052a761eea90b8d/pymongo-4.15.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c3fc24cb1f4ec60ed83162d4bba0c26abc6c9ae78c928805583673f3b3ea6984", size = 2351860, upload-time = "2025-12-02T18:43:42.002Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c5/fdcc81c20c67a61ba1073122c9ab42c937dd6f914004747e9ceefa4cead3/pymongo-4.15.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21d17bb2934b0640863361c08dd06991f128a97f9bee19425a499227be9ae6b4", size = 2251349, upload-time = "2025-12-02T18:43:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1c/e540ccac0685b234a23574dce3c8e077cd59bcb73ab19bcab1915894d3a6/pymongo-4.15.5-cp314-cp314-win32.whl", hash = "sha256:5a3974236cb842b4ef50a5a6bfad9c7d83a713af68ea3592ba240bbcb863305a", size = 992901, upload-time = "2025-12-02T18:43:45.732Z" }, - { url = "https://files.pythonhosted.org/packages/89/31/eb72c53bc897cb50b57000d71ce9bdcfc9c84ba4c7f6d55348df47b241d8/pymongo-4.15.5-cp314-cp314-win_amd64.whl", hash = "sha256:73fa8a7eee44fd95ba7d5cf537340ff3ff34efeb1f7d6790532d0a6ed4dee575", size = 1021205, upload-time = "2025-12-02T18:43:47.756Z" }, - { url = "https://files.pythonhosted.org/packages/ea/4a/74a7cc350d60953d27b5636906b43b232b501cee07f70f6513ac603097e8/pymongo-4.15.5-cp314-cp314-win_arm64.whl", hash = "sha256:d41288ca2a3eb9ac7c8cad4ea86ef8d63b69dc46c9b65c2bbd35331ec2a0fc57", size = 1000616, upload-time = "2025-12-02T18:43:49.677Z" }, - { url = "https://files.pythonhosted.org/packages/1a/22/1e557868b9b207d7dbf7706412251b28a82d4b958e007b6f2569d59ada3d/pymongo-4.15.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:552670f0c8bff103656d4e4b1f2c018f789c9de03f7615ed5e547d5b1b83cda0", size = 1086723, upload-time = "2025-12-02T18:43:51.432Z" }, - { url = "https://files.pythonhosted.org/packages/aa/9c/2e24c2da289e1d3b9bc4e0850136a364473bddfbe8b19b33d2bb5d30ee0d/pymongo-4.15.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41891b45f6ff1e23cfd1b7fbe40286664ad4507e2d2aa61c6d8c40eb6e11dded", size = 1086653, upload-time = "2025-12-02T18:43:53.131Z" }, - { url = "https://files.pythonhosted.org/packages/c6/be/4c2460c9ec91a891c754b91914ce700cc46009dae40183a85e26793dfae9/pymongo-4.15.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:524a8a593ae2eb1ec6db761daf0c03f98824e9882ab7df3d458d0c76c7ade255", size = 2531627, upload-time = "2025-12-02T18:43:55.141Z" }, - { url = "https://files.pythonhosted.org/packages/a0/48/cea56d04eb6bbd8b8943ff73d7cf26b94f715fccb23cf7ef9a4f853725a0/pymongo-4.15.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7ceb35c41b86711a1b284c604e2b944a2d46cb1b8dd3f8b430a9155491378f2", size = 2603767, upload-time = "2025-12-02T18:43:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ff/6743e351f8e0d5c3f388deb15f0cdbb77d2439eb3fba7ebcdf7878719517/pymongo-4.15.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3be2336715924be3a861b5e40c634376fd6bfe6dd1892d391566aa5a88a31307", size = 2725216, upload-time = "2025-12-02T18:43:59.463Z" }, - { url = "https://files.pythonhosted.org/packages/d4/90/fa532b6320b3ba61872110ff6f674bd54b54a592c0c64719e4f46852d0b6/pymongo-4.15.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d65df9c015e33f74ea9d1abf474971abca21e347a660384f8227dbdab75a33ca", size = 2704804, upload-time = "2025-12-02T18:44:01.415Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/1905c269aced043973b9528d94678e62e2eba249e70490c3c32dc70e2501/pymongo-4.15.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83c05bea05e151754357f8e6bbb80d5accead5110dc58f64e283173c71ec9de2", size = 2582274, upload-time = "2025-12-02T18:44:03.427Z" }, - { url = "https://files.pythonhosted.org/packages/7e/af/78c13179961e418396ec6ef53c0f1c855f1e9f1176d10909e8345d65366a/pymongo-4.15.5-cp314-cp314t-win32.whl", hash = "sha256:7c285614a3e8570b03174a25db642e449b0e7f77a6c9e487b73b05c9bf228ee6", size = 1044015, upload-time = "2025-12-02T18:44:05.318Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/49012f03418dce976124da339f3a6afbe6959cb0468ca6302596fe272926/pymongo-4.15.5-cp314-cp314t-win_amd64.whl", hash = "sha256:aae7d96f7b2b1a2753349130797543e61e93ee2ace8faa7fbe0565e2eb5d815f", size = 1078481, upload-time = "2025-12-02T18:44:07.215Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fc/f352a070d8ff6f388ce344c5ddb82348a38e0d1c99346fa6bfdef07134fe/pymongo-4.15.5-cp314-cp314t-win_arm64.whl", hash = "sha256:576a7d4b99465d38112c72f7f3d345f9d16aeeff0f923a3b298c13e15ab4f0ad", size = 1051166, upload-time = "2025-12-02T18:44:09.048Z" }, -] - [[package]] name = "pyperclip" version = "1.9.0" @@ -6240,76 +5840,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] -[[package]] -name = "ruamel-yaml" -version = "0.18.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/c7/ee630b29e04a672ecfc9b63227c87fd7a37eb67c1bf30fe95376437f897c/ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a", size = 147269, upload-time = "2025-10-22T17:54:02.346Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba", size = 119858, upload-time = "2025-10-22T17:53:59.012Z" }, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, - { url = "https://files.pythonhosted.org/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, - { url = "https://files.pythonhosted.org/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, - { url = "https://files.pythonhosted.org/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, - { url = "https://files.pythonhosted.org/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, - { url = "https://files.pythonhosted.org/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, - { url = "https://files.pythonhosted.org/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, - { url = "https://files.pythonhosted.org/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, - { url = "https://files.pythonhosted.org/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, - { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, - { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, - { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, - { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, - { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, - { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, - { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, - { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, - { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, - { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, - { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, - { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, - { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, - { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, - { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, - { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, - { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, - { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, - { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, -] - [[package]] name = "ruff" version = "0.9.10" @@ -6484,6 +6014,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, + { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + [[package]] name = "sse-starlette" version = "2.4.1" @@ -6645,15 +6224,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" }, ] -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, -] - [[package]] name = "temporalio" version = "1.17.0" From d54f91caa4b113664561de292af8cd4e108a3694 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 16 Dec 2025 16:08:20 -0800 Subject: [PATCH 04/16] use FireworkS SDK for Secret automation --- eval_protocol/platform_api.py | 183 ++++++++++++---------------------- 1 file changed, 62 insertions(+), 121 deletions(-) diff --git a/eval_protocol/platform_api.py b/eval_protocol/platform_api.py index bf608be0..407b6b64 100644 --- a/eval_protocol/platform_api.py +++ b/eval_protocol/platform_api.py @@ -1,9 +1,8 @@ # eval_protocol/platform_api.py import logging import sys -from typing import Any, Dict, Optional +from typing import Optional -import requests from dotenv import find_dotenv, load_dotenv from eval_protocol.auth import ( @@ -11,7 +10,8 @@ get_fireworks_api_base, get_fireworks_api_key, ) -from eval_protocol.common_utils import get_user_agent +from fireworks.types import Secret +from fireworks import Fireworks, FireworksError, NotFoundError, InternalServerError logger = logging.getLogger(__name__) @@ -88,47 +88,31 @@ def create_or_update_fireworks_secret( resolved_api_key = api_key or get_fireworks_api_key() resolved_api_base = api_base or get_fireworks_api_base() resolved_account_id = account_id # Must be provided + client = Fireworks(api_key=resolved_api_key, account_id=resolved_account_id, base_url=resolved_api_base) if not all([resolved_api_key, resolved_api_base, resolved_account_id]): logger.error("Missing Fireworks API key, base URL, or account ID for creating/updating secret.") return False - headers = { - "Authorization": f"Bearer {resolved_api_key}", - "Content-Type": "application/json", - "User-Agent": get_user_agent(), - } - - # The secret_id for GET/PATCH/DELETE operations is the key_name. - # The 'name' field in the gatewaySecret model for POST/PATCH is a bit ambiguous. - # For POST (create), the body is gatewaySecret, which has 'name', 'keyName', 'value'. - # 'name' in POST body is likely just the 'keyName' or 'secret_id' for creation context, - # as the full resource name 'accounts/.../secrets/...' is server-generated. - # Let's assume for POST, we send 'keyName' and 'value'. - # For PATCH, the path contains {secret_id} which is the key_name. The body is also gatewaySecret. - # Check if secret exists using GET (path uses normalized resource id) resource_id = _normalize_secret_resource_id(key_name) secret_exists = False try: - url = f"{resolved_api_base}/v1/accounts/{resolved_account_id}/secrets/{resource_id}" - response = requests.get(url, headers=headers, timeout=10) - if response.status_code == 200: + secret = client.secrets.get(resource_id) + if secret: secret_exists = True logger.info(f"Secret '{key_name}' already exists. Will attempt to update.") - elif response.status_code == 404: - logger.info(f"Secret '{key_name}' does not exist. Will attempt to create.") - secret_exists = False - elif response.status_code == 500: # As per user feedback, 500 on GET might mean not found - logger.warning( - f"Received 500 error when checking for secret '{key_name}'. Assuming it does not exist and will attempt to create. Response: {response.text}" - ) - secret_exists = False - else: - logger.error(f"Error checking for secret '{key_name}': {response.status_code} - {response.text}") - return False - except requests.exceptions.RequestException as e: - logger.error(f"Request exception while checking for secret '{key_name}': {e}") + except NotFoundError: + # Secret doesn't exist, proceed with creation + secret_exists = False + except InternalServerError as e: + # As per user feedback, 500 on GET might mean not found, treat as not found + logger.warning( + f"Received 500 error when checking for secret '{key_name}'. Assuming it doesn't exist. Response: {e}" + ) + secret_exists = False + except FireworksError as e: + logger.error(f"Error checking for secret '{key_name}': {e}") return False if secret_exists: @@ -144,31 +128,15 @@ def create_or_update_fireworks_secret( ) payload_key_name = "EP_SECRET" # Fallback, though unlikely needed with .upper() - payload = {"keyName": payload_key_name, "value": secret_value} try: - logger.debug(f"PATCH payload for '{key_name}': {payload}") - url = f"{resolved_api_base}/v1/accounts/{resolved_account_id}/secrets/{resource_id}" - response = requests.patch(url, json=payload, headers=headers, timeout=30) - response.raise_for_status() + logger.debug(f"PATCH payload for '{key_name}': key_name={payload_key_name}") + client.secrets.update(resource_id, key_name=payload_key_name, value=secret_value) logger.info(f"Successfully updated secret '{key_name}' on Fireworks platform.") return True - except requests.exceptions.HTTPError as e: - logger.error(f"HTTP error updating secret '{key_name}': {e.response.status_code} - {e.response.text}") - return False - except requests.exceptions.RequestException as e: - logger.error(f"Request exception updating secret '{key_name}': {e}") + except FireworksError as e: + logger.error(f"Error updating secret '{key_name}': {e}") return False else: - # Create new secret (POST) - # Body for POST is gatewaySecret. 'name' field in payload is the resource path. - # Let's assume for POST, the 'name' in payload can be omitted or is the key_name. - # The API should ideally use 'keyName' from URL or a specific 'secretId' in payload for creation if 'name' is server-assigned. - # Given the Swagger, 'name' is required in gatewaySecret. - # Let's try with 'name' being the 'key_name' for the payload, as the full path is not known yet. - # This might need adjustment based on actual API behavior. - # Construct the full 'name' path for the POST payload as per Swagger's title for 'name' - full_resource_name_for_payload = f"accounts/{resolved_account_id}/secrets/{resource_id}" - # Transform key_name for payload "keyName" field: uppercase and underscores payload_key_name = key_name.upper().replace("-", "_") if not payload_key_name or not payload_key_name[0].isupper(): @@ -177,26 +145,12 @@ def create_or_update_fireworks_secret( ) payload_key_name = "EP_SECRET" - payload = { - "name": full_resource_name_for_payload, # This 'name' is the resource path - "keyName": payload_key_name, # This 'keyName' is the specific field with new rules - "value": secret_value, - } try: - logger.debug(f"POST payload for '{key_name}': {payload}") - url = f"{resolved_api_base}/v1/accounts/{resolved_account_id}/secrets" - response = requests.post(url, json=payload, headers=headers, timeout=30) - response.raise_for_status() - logger.info( - f"Successfully created secret '{key_name}' on Fireworks platform. Full name: {response.json().get('name')}" - ) + logger.debug(f"POST payload for '{key_name}': {payload_key_name}") + client.secrets.create(key_name=payload_key_name, value=secret_value, name=resource_id) return True - except requests.exceptions.HTTPError as e: - logger.error(f"HTTP error creating secret '{key_name}': {e.response.status_code} - {e.response.text}") - # If error is due to 'name' field, this log will show it. - return False - except requests.exceptions.RequestException as e: - logger.error(f"Request exception creating secret '{key_name}': {e}") + except FireworksError as e: + logger.error(f"Error creating secret '{key_name}': {e}") return False @@ -205,7 +159,7 @@ def get_fireworks_secret( key_name: str, # This is the identifier for the secret api_key: Optional[str] = None, api_base: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[Secret]: """ Retrieves a secret from the Fireworks AI platform by its keyName. Note: This typically does not return the secret's actual value for security reasons, @@ -215,30 +169,25 @@ def get_fireworks_secret( resolved_api_base = api_base or get_fireworks_api_base() resolved_account_id = account_id - if not all([resolved_api_key, resolved_api_base, resolved_account_id]): - logger.error("Missing Fireworks API key, base URL, or account ID for getting secret.") - return None - - headers = { - "Authorization": f"Bearer {resolved_api_key}", - "User-Agent": get_user_agent(), - } + client = Fireworks(api_key=resolved_api_key, account_id=resolved_account_id, base_url=resolved_api_base) resource_id = _normalize_secret_resource_id(key_name) try: - url = f"{resolved_api_base}/v1/accounts/{resolved_account_id}/secrets/{resource_id}" - response = requests.get(url, headers=headers, timeout=10) - if response.status_code == 200: + secret = client.secrets.get(resource_id) + if secret: logger.info(f"Successfully retrieved secret '{key_name}'.") - return response.json() - elif response.status_code == 404: - logger.info(f"Secret '{key_name}' not found.") - return None - else: - logger.error(f"Error getting secret '{key_name}': {response.status_code} - {response.text}") - return None - except requests.exceptions.RequestException as e: - logger.error(f"Request exception while getting secret '{key_name}': {e}") + return secret + except NotFoundError: + logger.info(f"Secret '{key_name}' not found.") + return None + except InternalServerError as e: + # As per user feedback, 500 on GET might mean not found + logger.warning( + f"Received 500 error when getting secret '{key_name}'. Assuming it doesn't exist. Response: {e}" + ) + return None + except FireworksError as e: + logger.error(f"Error getting secret '{key_name}': {e}") return None @@ -259,33 +208,24 @@ def delete_fireworks_secret( logger.error("Missing Fireworks API key, base URL, or account ID for deleting secret.") return False - headers = { - "Authorization": f"Bearer {resolved_api_key}", - "User-Agent": get_user_agent(), - } + client = Fireworks(api_key=resolved_api_key, account_id=resolved_account_id, base_url=resolved_api_base) resource_id = _normalize_secret_resource_id(key_name) try: - url = f"{resolved_api_base}/v1/accounts/{resolved_account_id}/secrets/{resource_id}" - response = requests.delete(url, headers=headers, timeout=30) - if response.status_code == 200 or response.status_code == 204: # 204 No Content is also success for DELETE - logger.info(f"Successfully deleted secret '{key_name}'.") - return True - elif response.status_code == 404: - logger.info(f"Secret '{key_name}' not found, nothing to delete.") - return True - elif ( - response.status_code == 500 - ): # As per user feedback, 500 on GET might mean not found, apply same logic for DELETE - logger.warning( - f"Received 500 error when deleting secret '{key_name}'. Assuming it might not have existed. Response: {response.text}" - ) - return True # Consider deletion successful if it results in non-existence - else: - logger.error(f"Error deleting secret '{key_name}': {response.status_code} - {response.text}") - return False - except requests.exceptions.RequestException as e: - logger.error(f"Request exception while deleting secret '{key_name}': {e}") + client.secrets.delete(resource_id, account_id=resolved_account_id) + logger.info(f"Successfully deleted secret '{key_name}'.") + return True + except NotFoundError: + logger.info(f"Secret '{key_name}' not found, nothing to delete.") + return True + except InternalServerError as e: + # As per user feedback, 500 on GET might mean not found, apply same logic for DELETE + logger.warning( + f"Received 500 error when deleting secret '{key_name}'. Assuming it might not have existed. Response: {e}" + ) + return True # Consider deletion successful if it results in non-existence + except FireworksError as e: + logger.error(f"Error deleting secret '{key_name}': {e}") return False @@ -319,8 +259,6 @@ def delete_fireworks_secret( logger.error( "CRITICAL: FIREWORKS_API_KEY and FIREWORKS_API_BASE must be correctly set in environment or .env file to run this test." ) - import sys # Make sure sys is imported if using sys.exit - sys.exit(1) test_secret_key_name = "rewardkit-test-secret-delete-me" # Changed to be valid @@ -331,7 +269,7 @@ def delete_fireworks_secret( # 1. Ensure it doesn't exist initially (or delete if it does from a previous failed run) logger.info(f"\n[Test Step 0] Attempting to delete '{test_secret_key_name}' if it exists (cleanup)...") - delete_fireworks_secret(test_account_id, test_secret_key_name) + delete_fireworks_secret(account_id=test_account_id, key_name=test_secret_key_name) retrieved = get_fireworks_secret(test_account_id, test_secret_key_name) if retrieved is None: logger.info(f"Confirmed secret '{test_secret_key_name}' does not exist before creation test.") @@ -351,8 +289,11 @@ def delete_fireworks_secret( logger.info(f"Retrieved secret metadata: {retrieved_after_create}") # Assert against the transformed keyName that's expected in the payload/response body expected_payload_key_name = test_secret_key_name.upper().replace("-", "_") - assert retrieved_after_create.get("keyName") == expected_payload_key_name - assert retrieved_after_create.get("value") == test_secret_value # Also check value if returned + assert retrieved_after_create.key_name == expected_payload_key_name + # Note: value is typically not returned in GET responses for security reasons + # The value field will be None or empty string, so we don't assert on it + if retrieved_after_create.value: + logger.info(f"Note: Secret value was returned (unusual): {retrieved_after_create.value[:10]}...") else: logger.error(f"Failed to retrieve secret '{test_secret_key_name}' after creation.") From faa63bc53a74f5990a4a93c8264fd76b9d955ebd Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Mon, 22 Dec 2025 15:57:58 -0800 Subject: [PATCH 05/16] uv lock --- uv.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/uv.lock b/uv.lock index 75b38223..5d1059d1 100644 --- a/uv.lock +++ b/uv.lock @@ -1308,6 +1308,7 @@ requires-dist = [ { name = "e2b", marker = "extra == 'dev'" }, { name = "fastapi", specifier = ">=0.116.1" }, { name = "fireworks-ai", specifier = "==1.0.0a18" }, + { name = "fireworks-ai", specifier = ">=1.0.0a11" }, { name = "google-auth", marker = "extra == 'bigquery'", specifier = ">=2.0.0" }, { name = "google-cloud-bigquery", marker = "extra == 'bigquery'", specifier = ">=3.0.0" }, { name = "gymnasium", marker = "extra == 'dev'", specifier = ">=1.2.0" }, From a7fd3f439695bfa440efb50e2ea0ab04616cfc91 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 23 Dec 2025 10:23:18 -0800 Subject: [PATCH 06/16] update --- pyproject.toml | 3 +-- uv.lock | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 64630ae2..015349e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "pytest-asyncio>=0.21.0", "peewee>=3.18.2", "backoff>=2.2.0", - "fireworks-ai>=1.0.0a11", + "fireworks-ai==1.0.0a19", "questionary>=2.0.0", # Dependencies for vendored tau2 package "toml>=0.10.0", @@ -48,7 +48,6 @@ dependencies = [ "deepdiff>=6.0.0", "websockets>=15.0.1", "fastapi>=0.116.1", - "fireworks-ai==1.0.0a18", ] [project.urls] diff --git a/uv.lock b/uv.lock index c7b59e91..b2ec09bf 100644 --- a/uv.lock +++ b/uv.lock @@ -1309,8 +1309,7 @@ requires-dist = [ { name = "dspy", marker = "extra == 'dspy'", specifier = ">=3.0.0" }, { name = "e2b", marker = "extra == 'dev'" }, { name = "fastapi", specifier = ">=0.116.1" }, - { name = "fireworks-ai", specifier = "==1.0.0a18" }, - { name = "fireworks-ai", specifier = ">=1.0.0a11" }, + { name = "fireworks-ai", specifier = "==1.0.0a19" }, { name = "google-auth", marker = "extra == 'bigquery'", specifier = ">=2.0.0" }, { name = "google-cloud-bigquery", marker = "extra == 'bigquery'", specifier = ">=3.0.0" }, { name = "gymnasium", marker = "extra == 'dev'", specifier = ">=1.2.0" }, @@ -1579,7 +1578,7 @@ wheels = [ [[package]] name = "fireworks-ai" -version = "1.0.0a18" +version = "1.0.0a19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1591,9 +1590,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/c8/2d8454b01facbc3db73ec1a30d087ef2a3f6eee42b2817ca984cda5e789f/fireworks_ai-1.0.0a18.tar.gz", hash = "sha256:68a80a7ab15803a03cca96efc7078099e229901f867d43cbb463963ed2353ba2", size = 563750, upload-time = "2025-12-20T08:45:24.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/02/1a21aa42374d08d83280eb6f6fc34b1ec94eea55390d201e30c524840f24/fireworks_ai-1.0.0a19.tar.gz", hash = "sha256:a8194bc306d3c5cc1bbe3484c01e0975ce9cace180971de57ebb2725a0064dc2", size = 565546, upload-time = "2025-12-22T23:33:33.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b0/eaa7b865a99307e80fd6852348dc0aa1135c0e8c5cb91b050f93dde4919d/fireworks_ai-1.0.0a18-py3-none-any.whl", hash = "sha256:7f6b46a2b9928464bc515f3f68282a97e2df2f6e230ea1acc26d9c4d404a0f6e", size = 307477, upload-time = "2025-12-20T08:45:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0e/46/0bf7a6dbcacebd967467f98442a367c59b6c67aa71f82236ca5d932e3770/fireworks_ai-1.0.0a19-py3-none-any.whl", hash = "sha256:2e3cb102a2f65c3b8a6eb5daaa71438c723e72d1f5689e8a86fa3b685d32e74f", size = 309072, upload-time = "2025-12-22T23:33:31.886Z" }, ] [[package]] From 84f7d14899520470cf1062691a60b9af0c71dc2b Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 23 Dec 2025 13:24:15 -0800 Subject: [PATCH 07/16] use latest SDK --- eval_protocol/cli.py | 61 ++++++++++++++++++++++++++++++++------------ pyproject.toml | 2 +- uv.lock | 8 +++--- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/eval_protocol/cli.py b/eval_protocol/cli.py index a2278a31..90fdd9f1 100644 --- a/eval_protocol/cli.py +++ b/eval_protocol/cli.py @@ -332,6 +332,8 @@ def _configure_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParse "upload", help="Scan for evaluation tests, select, and upload as Fireworks evaluators", ) + + # CLI workflow flags (not part of the SDK create() signature) upload_parser.add_argument( "--path", default=".", @@ -341,23 +343,6 @@ def _configure_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParse "--entry", help="Entrypoint of evaluation test to upload (module:function or path::function). For multiple, separate by commas.", ) - upload_parser.add_argument( - "--id", - help="Evaluator ID to use (if multiple selections, a numeric suffix is appended)", - ) - upload_parser.add_argument( - "--display-name", - help="Display name for evaluator (defaults to ID)", - ) - upload_parser.add_argument( - "--description", - help="Description for evaluator", - ) - upload_parser.add_argument( - "--force", - action="store_true", - help="Overwrite existing evaluator with the same ID", - ) upload_parser.add_argument( "--yes", "-y", @@ -368,6 +353,48 @@ def _configure_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParse "--env-file", help="Path to .env file containing secrets to upload (default: .env in current directory)", ) + upload_parser.add_argument( + "--force", + action="store_true", + help="Overwrite existing evaluator with the same ID", + ) + + # Auto-generate flags from SDK Fireworks().evaluators.create() signature + create_evaluator_fn = Fireworks().evaluators.create + + upload_skip_fields = { + "__top_level__": { + "account_id", # auto-detected + "extra_headers", + "extra_query", + "extra_body", + "timeout", + }, + "evaluator": { + "commit_hash", # should be auto-detected from git + "source", # not relevant for CLI flow + }, + } + upload_aliases = { + "evaluator_id": ["--id"], + "evaluator.display_name": ["--name"], + } + upload_help_overrides = { + "evaluator_id": "Evaluator ID to use (if multiple selections, a numeric suffix is appended)", + "evaluator.display_name": "Display name for evaluator (defaults to ID)", + "evaluator.description": "Description for evaluator", + "evaluator.requirements": "Requirements for evaluator (auto-detected from requirements.txt if not provided)", + "evaluator.entry_point": "Pytest-style entrypoint (e.g., test_file.py::test_func). Auto-detected if not provided.", + "evaluator.default_dataset": "Default dataset to use with this evaluator", + } + + add_args_from_callable_signature( + upload_parser, + create_evaluator_fn, + skip_fields=upload_skip_fields, + aliases=upload_aliases, + help_overrides=upload_help_overrides, + ) # Create command group create_parser = subparsers.add_parser( diff --git a/pyproject.toml b/pyproject.toml index 015349e4..bad2683c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "pytest-asyncio>=0.21.0", "peewee>=3.18.2", "backoff>=2.2.0", - "fireworks-ai==1.0.0a19", + "fireworks-ai==1.0.0a20", "questionary>=2.0.0", # Dependencies for vendored tau2 package "toml>=0.10.0", diff --git a/uv.lock b/uv.lock index b2ec09bf..917c1ff6 100644 --- a/uv.lock +++ b/uv.lock @@ -1309,7 +1309,7 @@ requires-dist = [ { name = "dspy", marker = "extra == 'dspy'", specifier = ">=3.0.0" }, { name = "e2b", marker = "extra == 'dev'" }, { name = "fastapi", specifier = ">=0.116.1" }, - { name = "fireworks-ai", specifier = "==1.0.0a19" }, + { name = "fireworks-ai", specifier = "==1.0.0a20" }, { name = "google-auth", marker = "extra == 'bigquery'", specifier = ">=2.0.0" }, { name = "google-cloud-bigquery", marker = "extra == 'bigquery'", specifier = ">=3.0.0" }, { name = "gymnasium", marker = "extra == 'dev'", specifier = ">=1.2.0" }, @@ -1578,7 +1578,7 @@ wheels = [ [[package]] name = "fireworks-ai" -version = "1.0.0a19" +version = "1.0.0a20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1590,9 +1590,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/02/1a21aa42374d08d83280eb6f6fc34b1ec94eea55390d201e30c524840f24/fireworks_ai-1.0.0a19.tar.gz", hash = "sha256:a8194bc306d3c5cc1bbe3484c01e0975ce9cace180971de57ebb2725a0064dc2", size = 565546, upload-time = "2025-12-22T23:33:33.291Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/c6/cdc6c152876ee1253491e6f72c65c2cdaf7b22b320be0cec7ac5778d3b1c/fireworks_ai-1.0.0a20.tar.gz", hash = "sha256:c84f702445679ea768461dba8fb027175b82255021832a89f9ece65821a2ab25", size = 564097, upload-time = "2025-12-23T19:21:17.891Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/46/0bf7a6dbcacebd967467f98442a367c59b6c67aa71f82236ca5d932e3770/fireworks_ai-1.0.0a19-py3-none-any.whl", hash = "sha256:2e3cb102a2f65c3b8a6eb5daaa71438c723e72d1f5689e8a86fa3b685d32e74f", size = 309072, upload-time = "2025-12-22T23:33:31.886Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/e2bc9c4af291786bc7fe364ae63503ba2c8161c2e71223d570a77f0a1415/fireworks_ai-1.0.0a20-py3-none-any.whl", hash = "sha256:b5e199978f71b564b2e19cf55a71c1ac20906d9a7b4ae75135fdccb245227722", size = 304153, upload-time = "2025-12-23T19:21:15.943Z" }, ] [[package]] From 17f99f9c14391dfa61f667b6459a8e3430283242 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 23 Dec 2025 13:24:57 -0800 Subject: [PATCH 08/16] update --- eval_protocol/platform_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/eval_protocol/platform_api.py b/eval_protocol/platform_api.py index 407b6b64..94a5f108 100644 --- a/eval_protocol/platform_api.py +++ b/eval_protocol/platform_api.py @@ -177,6 +177,9 @@ def get_fireworks_secret( if secret: logger.info(f"Successfully retrieved secret '{key_name}'.") return secret + else: + logger.warning(f"Secret '{key_name}' lookup succeeded but returned empty/falsy value.") + return None except NotFoundError: logger.info(f"Secret '{key_name}' not found.") return None From 5a128207d33aa5e738ebad9f7cb1ab77dc06e601 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 23 Dec 2025 13:25:44 -0800 Subject: [PATCH 09/16] update --- eval_protocol/platform_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eval_protocol/platform_api.py b/eval_protocol/platform_api.py index 94a5f108..a950a0d4 100644 --- a/eval_protocol/platform_api.py +++ b/eval_protocol/platform_api.py @@ -169,6 +169,10 @@ def get_fireworks_secret( resolved_api_base = api_base or get_fireworks_api_base() resolved_account_id = account_id + if not all([resolved_api_key, resolved_api_base, resolved_account_id]): + logger.error("Missing Fireworks API key, base URL, or account ID for getting secret.") + return None + client = Fireworks(api_key=resolved_api_key, account_id=resolved_account_id, base_url=resolved_api_base) resource_id = _normalize_secret_resource_id(key_name) From 82ffc3aa66974b232e12e18ef4afd7fc41d7feb9 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 23 Dec 2025 14:33:54 -0800 Subject: [PATCH 10/16] update --- eval_protocol/platform_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eval_protocol/platform_api.py b/eval_protocol/platform_api.py index a950a0d4..60743ccb 100644 --- a/eval_protocol/platform_api.py +++ b/eval_protocol/platform_api.py @@ -286,7 +286,7 @@ def delete_fireworks_secret( # 2. Create secret logger.info(f"\n[Test Step 1] Creating secret '{test_secret_key_name}' with value '{test_secret_value}'...") - success_create = create_or_update_fireworks_secret(test_account_id, test_secret_key_name, test_secret_value) + success_create: bool = create_or_update_fireworks_secret(test_account_id, test_secret_key_name, test_secret_value) logger.info(f"Create operation success: {success_create}") # 3. Get secret (to verify creation, though value won't be returned) From a2e955776f8b0f91fdca7c439e65d4456662a908 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 23 Dec 2025 15:42:49 -0800 Subject: [PATCH 11/16] Refactor _load_secrets_from_env_file to return all environment variables and remove API_KEY filtering logic --- eval_protocol/cli_commands/upload.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/eval_protocol/cli_commands/upload.py b/eval_protocol/cli_commands/upload.py index c978d48c..ceb38a86 100644 --- a/eval_protocol/cli_commands/upload.py +++ b/eval_protocol/cli_commands/upload.py @@ -136,8 +136,6 @@ def _resolve_entry_to_qual_and_source(entry: str, cwd: str) -> tuple[str, str]: def _load_secrets_from_env_file(env_file_path: str) -> Dict[str, str]: """ Load secrets from a .env file that should be uploaded to Fireworks. - - Returns a dictionary of secret key-value pairs that contain 'API_KEY' in the name. """ if not os.path.exists(env_file_path): return {} @@ -152,14 +150,7 @@ def _load_secrets_from_env_file(env_file_path: str) -> Dict[str, str]: key = key.strip() value = value.strip().strip('"').strip("'") # Remove quotes env_vars[key] = value - - # Filter for secrets that look like API keys - secrets = {} - for key, value in env_vars.items(): - if "API_KEY" in key.upper() and value: - secrets[key] = value - - return secrets + return env_vars def _mask_secret_value(value: str) -> str: @@ -193,13 +184,6 @@ def upload_command(args: argparse.Namespace) -> int: selected_tests = _discover_and_select_tests(root, non_interactive=non_interactive) if not selected_tests: return 1 - # Warn about parameterized tests - parameterized_tests = [t for t in selected_tests if t.has_parametrize] - if parameterized_tests: - print("\nNote: Parameterized tests will be uploaded as a single evaluator that") - print(" handles all parameter combinations. The evaluator will work with") - print(" the same logic regardless of which model/parameters are used.") - selected_specs = [(t.qualname, t.file_path) for t in selected_tests] base_id = getattr(args, "id", None) From b97d35e5a1df157f705a5a82f3ce7f941b2fb0d8 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Wed, 24 Dec 2025 10:44:47 -0800 Subject: [PATCH 12/16] remove legacy reward kit evaluator upload code --- eval_protocol/cli.py | 393 +-------- eval_protocol/cli_commands/deploy.py | 509 ----------- eval_protocol/cli_commands/deploy_mcp.py | 290 ------- eval_protocol/cli_commands/preview.py | 186 ---- eval_protocol/evaluation.py | 860 ++----------------- tests/cli_commands/test_deploy_cmd.py | 507 ----------- tests/cli_commands/test_preview_cmd.py | 218 ----- tests/test_cli.py | 171 ---- tests/test_cli_args.py | 156 +--- tests/test_deploy_integration.py | 215 ----- tests/test_evaluation.py | 375 ++------ tests/test_evaluation_integration.py | 365 -------- tests/test_evaluation_preview_integration.py | 470 ---------- 13 files changed, 129 insertions(+), 4586 deletions(-) delete mode 100644 eval_protocol/cli_commands/deploy.py delete mode 100644 eval_protocol/cli_commands/deploy_mcp.py delete mode 100644 eval_protocol/cli_commands/preview.py delete mode 100644 tests/cli_commands/test_deploy_cmd.py delete mode 100644 tests/cli_commands/test_preview_cmd.py delete mode 100644 tests/test_cli.py delete mode 100644 tests/test_deploy_integration.py delete mode 100644 tests/test_evaluation_integration.py delete mode 100644 tests/test_evaluation_preview_integration.py diff --git a/eval_protocol/cli.py b/eval_protocol/cli.py index 90fdd9f1..ac8a8d9d 100644 --- a/eval_protocol/cli.py +++ b/eval_protocol/cli.py @@ -3,38 +3,17 @@ """ import argparse -import inspect -import json import logging import os import sys from pathlib import Path -from typing import Any, cast -from .cli_commands.utils import add_args_from_callable_signature from fireworks import Fireworks -logger = logging.getLogger(__name__) - - from .cli_commands.common import setup_logging +from .cli_commands.utils import add_args_from_callable_signature -# Re-export deploy_command for backward compatibility with tests importing from eval_protocol.cli -try: # pragma: no cover - import-time alias for tests - from .cli_commands import deploy as _deploy_mod - - deploy_command = _deploy_mod.deploy_command # type: ignore[attr-defined] -except Exception: # pragma: no cover - # If import fails in constrained environments, tests that import it will surface the issue - deploy_command = None # type: ignore[assignment] - -# Re-export preview_command for backward compatibility with tests importing from eval_protocol.cli -try: # pragma: no cover - import-time alias for tests - from .cli_commands import preview as _preview_mod - - preview_command = _preview_mod.preview_command # type: ignore[attr-defined] -except Exception: # pragma: no cover - preview_command = None # type: ignore[assignment] +logger = logging.getLogger(__name__) def build_parser() -> argparse.ArgumentParser: @@ -55,257 +34,6 @@ def _configure_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParse subparsers = parser.add_subparsers(dest="command", help="Command to run") - # NOTE: The following commands are hidden/disabled. Uncomment to re-enable. - # # Preview command - # preview_parser = subparsers.add_parser("preview", help="Preview an evaluator with sample data") - # preview_parser.add_argument( - # "--metrics-folders", - # "-m", - # nargs="+", - # help="Metric folders in format 'name=path', e.g., 'clarity=./metrics/clarity'", - # ) - # - # # Make samples optional to allow HF dataset option - # preview_parser.add_argument( - # "--samples", - # "-s", - # required=False, - # help="Path to JSONL file containing sample data", - # ) - # preview_parser.add_argument( - # "--max-samples", - # type=int, - # default=5, - # help="Maximum number of samples to process (default: 5)", - # ) - # - # # Add HuggingFace dataset options - # hf_group = preview_parser.add_argument_group("HuggingFace Dataset Options") - # hf_group.add_argument( - # "--huggingface-dataset", - # "--hf", - # help="HuggingFace dataset name (e.g., 'deepseek-ai/DeepSeek-ProverBench')", - # ) - # hf_group.add_argument( - # "--huggingface-split", - # default="train", - # help="Dataset split to use (default: 'train')", - # ) - # hf_group.add_argument( - # "--huggingface-prompt-key", - # default="prompt", - # help="Key in the dataset containing the prompt text (default: 'prompt')", - # ) - # hf_group.add_argument( - # "--huggingface-response-key", - # default="response", - # help="Key in the dataset containing the response text (default: 'response')", - # ) - # hf_group.add_argument( - # "--huggingface-key-map", - # help="JSON mapping of dataset keys to Eval Protocol message keys", - # ) - # preview_parser.add_argument( - # "--remote-url", - # help="URL of a remote reward function endpoint to preview against. If provided, metrics-folders might be ignored.", - # ) - # - # # Deploy command - # deploy_parser = subparsers.add_parser("deploy", help="Create and deploy an evaluator, or register a remote one") - # deploy_parser.add_argument("--id", required=True, help="ID for the evaluator") - # deploy_parser.add_argument( - # "--metrics-folders", - # "-m", - # nargs="+", - # required=False, # No longer strictly required if --remote-url is used - # help="Metric folders in format 'name=path', e.g., 'clarity=./metrics/clarity'. Required if not using --remote-url.", - # ) - # deploy_parser.add_argument( - # "--display-name", - # help="Display name for the evaluator (defaults to ID if not provided)", - # ) - # deploy_parser.add_argument("--description", help="Description for the evaluator") - # deploy_parser.add_argument( - # "--force", - # "-f", - # action="store_true", - # help="Force update if evaluator already exists", - # ) - # - # # Add HuggingFace dataset options to deploy command - # hf_deploy_group = deploy_parser.add_argument_group("HuggingFace Dataset Options") - # hf_deploy_group.add_argument( - # "--huggingface-dataset", - # "--hf", - # help="HuggingFace dataset name (e.g., 'deepseek-ai/DeepSeek-ProverBench')", - # ) - # hf_deploy_group.add_argument( - # "--huggingface-split", - # default="train", - # help="Dataset split to use (default: 'train')", - # ) - # hf_deploy_group.add_argument( - # "--huggingface-prompt-key", - # default="prompt", - # help="Key in the dataset containing the prompt text (default: 'prompt')", - # ) - # hf_deploy_group.add_argument( - # "--huggingface-response-key", - # default="response", - # help="Key in the dataset containing the response text (default: 'response')", - # ) - # hf_deploy_group.add_argument( - # "--huggingface-key-map", - # help="JSON mapping of dataset keys to Eval Protocol message keys", - # ) - # deploy_parser.add_argument( - # "--remote-url", - # help="URL of a pre-deployed remote reward function. If provided, deploys by registering this URL with Fireworks AI.", - # ) - # - # # Deployment target options - # target_group = deploy_parser.add_argument_group("Deployment Target Options") - # target_group.add_argument( - # "--target", - # choices=["fireworks", "gcp-cloud-run", "local-serve"], - # default="fireworks", - # help="Deployment target. 'fireworks' for standard Fireworks platform deployment, 'gcp-cloud-run' for Google Cloud Run, 'local-serve' for local serving with Serveo tunneling.", - # ) - # target_group.add_argument( - # "--function-ref", - # help="Reference to the reward function to deploy (e.g., 'my_module.reward_func'). Required for 'gcp-cloud-run' and 'local-serve' targets.", - # ) - # - # # Local serving options (relevant if --target is local-serve) - # local_serve_group = deploy_parser.add_argument_group("Local Serving Options (used if --target is local-serve)") - # local_serve_group.add_argument( - # "--local-port", - # type=int, - # default=8001, - # help="Port for the local reward function server to listen on (default: 8001). Used with --target local-serve.", - # ) - # - # # GCP deployment options - # gcp_group = deploy_parser.add_argument_group( - # "GCP Cloud Run Deployment Options (used if --target is gcp-cloud-run)" - # ) - # # --function-ref is now in target_group - # gcp_group.add_argument( - # "--gcp-project", - # required=False, - # help="Google Cloud Project ID. Must be provided via CLI or rewardkit.yaml.", - # ) - # gcp_group.add_argument( - # "--gcp-region", - # required=False, - # help="Google Cloud Region for deployment (e.g., 'us-central1'). Must be provided via CLI or rewardkit.yaml.", - # ) - # gcp_group.add_argument( - # "--gcp-ar-repo", - # required=False, - # help="Google Artifact Registry repository name. Optional, defaults to value in rewardkit.yaml or 'eval-protocol-evaluators' if not specified.", - # ) - # gcp_group.add_argument( - # "--service-account", - # help="Email of the GCP service account to run the Cloud Run service. Optional.", - # ) - # gcp_group.add_argument( - # "--entry-point", - # default="reward_function", - # help="The name of the entry point function within your --function-ref module (default: reward_function). Only for gcp-cloud-run.", - # ) - # gcp_group.add_argument( - # "--runtime", - # default="python311", # Or a sensible default - # help="The Cloud Functions/Run runtime (e.g., python311). Only for gcp-cloud-run.", - # ) - # gcp_group.add_argument( - # "--gcp-auth-mode", - # choices=["open", "api-key"], # Add 'iam' later - # default=None, # Default will be resolved in deploy_command - # help="Authentication mode for the deployed GCP Cloud Run service. " - # "'open': Publicly accessible. " - # "'api-key': Service is publicly accessible but requires an API key in requests (handled by the application). " - # "If not specified, defaults to value in rewardkit.yaml or 'api-key'. Optional.", - # ) - # - # # Deploy MCP command - # deploy_mcp_parser = subparsers.add_parser("deploy-mcp", help="Deploy an MCP server to Google Cloud Run") - # deploy_mcp_parser.add_argument("--id", required=True, help="Unique ID for the MCP server deployment") - # deploy_mcp_parser.add_argument( - # "--mcp-server-module", - # help="Python module containing the MCP server (e.g., 'examples.frozen_lake_mcp.frozen_lake_mcp_server'). Required if --dockerfile is not provided.", - # ) - # deploy_mcp_parser.add_argument( - # "--dockerfile", - # help="Path to Dockerfile to use for deployment (recommended for tested local Dockerfiles). When provided, --mcp-server-module is not required.", - # ) - # deploy_mcp_parser.add_argument( - # "--gcp-project", - # help="Google Cloud Project ID. Can also be set in rewardkit.yaml", - # ) - # deploy_mcp_parser.add_argument( - # "--gcp-region", - # help="Google Cloud Region (e.g., 'us-central1'). Can also be set in rewardkit.yaml", - # ) - # deploy_mcp_parser.add_argument( - # "--gcp-ar-repo", - # help="Google Artifact Registry repository name. Defaults to 'eval-protocol-mcp-servers'", - # ) - # deploy_mcp_parser.add_argument( - # "--port", - # type=int, - # default=8000, - # help="Port for the MCP server to listen on (default: 8000)", - # ) - # deploy_mcp_parser.add_argument( - # "--python-version", - # default="3.11", - # help="Python version for the container (default: 3.11)", - # ) - # deploy_mcp_parser.add_argument("--requirements", help="Additional pip requirements (newline separated)") - # deploy_mcp_parser.add_argument("--env-vars", nargs="*", help="Environment variables in KEY=VALUE format") - # - # # Agent-eval command - # agent_eval_parser = subparsers.add_parser( - # "agent-eval", help="Run agent evaluation using the ForkableResource framework." - # ) - # agent_eval_parser.add_argument( - # "--task-def", - # required=True, - # help="Path to task definition file or directory containing task definitions.", - # ) - # agent_eval_parser.add_argument( - # "--parallel", - # action="store_true", - # help="Execute tasks in parallel when multiple tasks are specified.", - # ) - # agent_eval_parser.add_argument( - # "--max-concurrency", - # type=int, - # default=3, - # help="Maximum number of tasks to execute in parallel (default: 3).", - # ) - # agent_eval_parser.add_argument( - # "--filter", - # nargs="+", - # help="Run only tasks matching the specified task IDs.", - # ) - # agent_eval_parser.add_argument( - # "--output-dir", - # default="./agent_runs", - # help="Directory to store agent evaluation run results (default: ./agent_runs).", - # ) - # agent_eval_parser.add_argument( - # "--model", - # help="Override MODEL_AGENT environment variable (format: provider/model_name).", - # ) - # agent_eval_parser.add_argument( - # "--num-rollouts", - # type=int, - # help="Override the number of parallel rollouts to execute for each task.", - # ) - # Logs command logs_parser = subparsers.add_parser("logs", help="Serve logs with file watching and real-time updates") logs_parser.add_argument("--port", type=int, default=8000, help="Port to bind to (default: 8000)") @@ -511,14 +239,6 @@ def _configure_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParse help="Extra flags to pass to 'docker run' (quoted string, e.g. \"--env-file .env --memory=8g\")", ) - # # Run command (for Hydra-based evaluations) - # # This subparser intentionally defines no arguments itself. - # # All arguments after 'run' will be passed to Hydra by parse_known_args. - # subparsers.add_parser( - # "run", - # help="Run an evaluation using a Hydra configuration. All arguments after 'run' are passed to Hydra.", - # ) - # Hidden command: export-docs (for generating CLI reference documentation) export_docs_parser = subparsers.add_parser("export-docs", help=argparse.SUPPRESS) export_docs_parser.add_argument( @@ -610,30 +330,10 @@ def _extract_flag_value(argv_list, flag_name): logger.debug("Using Fireworks API base: %s", normalized) # Now parse args normally (so help/commands work), after globals applied - # Store original sys.argv[0] because Hydra might manipulate it - # and we need it if we're not calling a Hydra app. - original_script_name = sys.argv[0] - args, remaining_argv = parse_args() # Use parse_known_args + args, _ = parse_args() setup_logging(args.verbose, getattr(args, "debug", False)) - # NOTE: The following command handlers are disabled. Uncomment to re-enable. - # if args.command == "preview": - # if preview_command is None: - # raise ImportError("preview_command is unavailable") - # return preview_command(args) - # elif args.command == "deploy": - # if deploy_command is None: - # raise ImportError("deploy_command is unavailable") - # return deploy_command(args) - # elif args.command == "deploy-mcp": - # from .cli_commands.deploy_mcp import deploy_mcp_command - # - # return deploy_mcp_command(args) - # elif args.command == "agent-eval": - # from .cli_commands.agent_eval_cmd import agent_eval_command - # - # return agent_eval_command(args) if args.command == "logs": from .cli_commands.logs import logs_command @@ -657,92 +357,9 @@ def _extract_flag_value(argv_list, flag_name): from .cli_commands.export_docs import export_docs_command return export_docs_command(args) - # elif args.command == "run": - # # For the 'run' command, Hydra takes over argument parsing. - # - # # Filter out the initial '--' if present in remaining_argv, which parse_known_args might add - # hydra_specific_args = [arg for arg in remaining_argv if arg != "--"] - # - # # Auto-detect local conf directory and add it to config path if not explicitly provided - # has_config_path = any(arg.startswith("--config-path") for arg in hydra_specific_args) - # current_dir = os.getcwd() - # local_conf_dir = os.path.join(current_dir, "conf") - # - # if not has_config_path and os.path.isdir(local_conf_dir): - # logger.info("Auto-detected local conf directory: %s", local_conf_dir) - # hydra_specific_args = [ - # "--config-path", - # local_conf_dir, - # ] + hydra_specific_args - # - # processed_hydra_args = [] - # i = 0 - # while i < len(hydra_specific_args): - # arg = hydra_specific_args[i] - # if arg == "--config-path": - # processed_hydra_args.append(arg) - # i += 1 - # if i < len(hydra_specific_args): - # path_val = hydra_specific_args[i] - # abs_path = os.path.abspath(path_val) - # logger.debug( - # "Converting relative --config-path '%s' (space separated) to absolute '%s'", - # path_val, - # abs_path, - # ) - # processed_hydra_args.append(abs_path) - # else: - # logger.error("--config-path specified without a value.") - # elif arg.startswith("--config-path="): - # flag_part, path_val = arg.split("=", 1) - # processed_hydra_args.append(flag_part) - # abs_path = os.path.abspath(path_val) - # logger.debug( - # "Converting relative --config-path '%s' (equals separated) to absolute '%s'", - # path_val, - # abs_path, - # ) - # processed_hydra_args.append(abs_path) - # else: - # processed_hydra_args.append(arg) - # i += 1 - # - # sys.argv = [sys.argv[0]] + processed_hydra_args - # logger.info("SYSCALL_ARGV_FOR_HYDRA (after potential abspath conversion): %s", sys.argv) - # - # try: - # from .cli_commands.run_eval_cmd import hydra_cli_entry_point - # - # hydra_entry = cast(Any, hydra_cli_entry_point) - # hydra_entry() # type: ignore # pylint: disable=no-value-for-parameter - # return 0 - # except Exception as e: # pylint: disable=broad-except - # error_msg = str(e) - # logger.error("Evaluation failed: %s", e) - # - # # Provide helpful suggestions for common Hydra/config errors - # if "Cannot find primary config" in error_msg: - # logger.error("HINT: Configuration file not found.") - # logger.error("SOLUTION: Ensure you have a config file in ./conf/ directory") - # logger.error("Try: eval-protocol run --config-name simple_uipath_eval") - # elif "missing from config" in error_msg or "MissingMandatoryValue" in error_msg: - # logger.error("HINT: Required configuration values are missing.") - # logger.error("SOLUTION: Check your config file for missing required fields") - # elif "Config search path" in error_msg: - # logger.error("HINT: Hydra cannot find the configuration directory.") - # logger.error("SOLUTION: Create a ./conf directory with your config files") - # elif "ValidationError" in error_msg: - # logger.error("HINT: Configuration validation failed.") - # logger.error("SOLUTION: Run 'eval-protocol validate-data --file your_data.jsonl' to check data") - # - # logger.error("\nQuick fix suggestions:") - # logger.error("1. Use the simplified setup: eval-protocol run --config-name simple_uipath_eval") - # logger.error("2. Validate your data first: eval-protocol validate-data --file data.jsonl --schema agent") - # logger.error("3. Ensure you have: ./conf/simple_uipath_eval.yaml and ./uipath_reward.py") - # return 1 else: - temp_parser = argparse.ArgumentParser(prog=os.path.basename(original_script_name)) - temp_parser.print_help() + parser = build_parser() + parser.print_help() return 1 diff --git a/eval_protocol/cli_commands/deploy.py b/eval_protocol/cli_commands/deploy.py deleted file mode 100644 index 1ae0313b..00000000 --- a/eval_protocol/cli_commands/deploy.py +++ /dev/null @@ -1,509 +0,0 @@ -""" -CLI command for creating and deploying an evaluator, -or registering a pre-deployed remote evaluator. -""" - -import importlib # For dynamically importing modules -import json -import os # For os.path.join, os.makedirs, os.getcwd (already imported but good to be explicit if used extensively) -import secrets # For API key generation (already imported but good to be explicit) -import sys # For sys.executable -import time # For sleep -from pathlib import Path # For path operations -from typing import Any, Dict - -import yaml # For saving config if save_config helper doesn't exist - -# TODO: Consider moving subprocess_manager functions to a more central location if used by core CLI -try: - # Import functions with explicit names to match expected signatures - from development.utils.subprocess_manager import ( - start_ngrok_and_get_url as _start_ngrok_and_get_url, - start_process as _start_process, - start_serveo_and_get_url as _start_serveo_and_get_url, - stop_process as _stop_process, - ) -except ImportError: - # Fallback implementations when development module is not available - import signal - import socket - import subprocess - - def _fallback_start_process(command, log_path, env=None): - """Fallback process starter.""" - try: - with open(log_path, "w") as log_file: - process = subprocess.Popen(command, stdout=log_file, stderr=subprocess.STDOUT, env=env) - return process - except Exception as e: - print(f"Error starting process: {e}") - return None - - def _fallback_stop_process(pid): - """Fallback process stopper.""" - try: - import os - - os.kill(pid, signal.SIGTERM) - except Exception: - pass - - def _fallback_start_serveo_and_get_url(local_port, log_path): - """Fallback serveo tunnel - returns None to indicate unavailable.""" - print("Serveo tunneling not available - development module not found") - return None, None - - def _fallback_start_ngrok_and_get_url(local_port, log_path): - """Fallback ngrok tunnel - returns None to indicate unavailable.""" - print("ngrok tunneling not available - development module not found") - return None, None - - # Expose unified names using fallbacks - start_process = _fallback_start_process - stop_process = _fallback_stop_process - start_serveo_and_get_url = _fallback_start_serveo_and_get_url - start_ngrok_and_get_url = _fallback_start_ngrok_and_get_url -else: - # Wrap imported helpers to present consistent, simple signatures used below - def start_process(command, log_path, env=None): - return _start_process(command=command, log_file_path=log_path, env=env) - - def stop_process(pid): - return _stop_process(pid) - - def start_serveo_and_get_url(local_port, log_path): - return _start_serveo_and_get_url(local_port=local_port, log_file_path=log_path) - - def start_ngrok_and_get_url(local_port, log_path): - return _start_ngrok_and_get_url(local_port=local_port, ngrok_log_file=log_path) - - -from eval_protocol.auth import get_fireworks_account_id -from eval_protocol.config import ( - GCPCloudRunConfig, - RewardKitConfig, - _config_file_path as global_loaded_config_path, - get_config, -) -from eval_protocol.evaluation import create_evaluation -from eval_protocol.gcp_tools import ( - build_and_push_docker_image, - deploy_to_cloud_run, - ensure_artifact_registry_repo_exists, - ensure_gcp_secret, -) -from eval_protocol.packaging import generate_dockerfile_content -from eval_protocol.platform_api import ( # For catching errors from create_evaluation - PlatformAPIError, - create_or_update_fireworks_secret, -) - -from .common import check_environment - - -def _establish_local_server_and_tunnel(args): - """ - Handles starting the local generic server and establishing a public tunnel - using Serveo, with a fallback to ngrok. - Returns: (public_url, tunnel_provider_name, local_server_pid, tunnel_process_pid) - Returns (None, None, server_pid_or_None, None) if tunneling fails. - """ - if not args.function_ref: - print("Error: --function-ref is required for local-serve target.") - return None, None, None, None - - evaluator_id = args.id - function_ref = args.function_ref - local_server_port = args.local_port - - log_dir = os.path.join(os.getcwd(), "logs", "eval-protocol-local") - os.makedirs(log_dir, exist_ok=True) - generic_server_log_path = os.path.join(log_dir, f"generic_server_{evaluator_id}.log") - - server_env = None # Run local server without API key protection - print(f"Note: Local server for '{evaluator_id}' will run without API key protection.") - - print(f"Starting local reward function server for '{function_ref}' on port {local_server_port}...") - server_command = [ - sys.executable, - "-m", - "eval_protocol.generic_server", - function_ref, - "--port", - str(local_server_port), - ] - - local_server_process = start_process(server_command, generic_server_log_path, env=server_env) - - if not local_server_process or local_server_process.poll() is not None: - print(f"Error: Failed to start local generic server. Check log: {generic_server_log_path}") - return None, None, None, None # No server, no tunnel - - local_server_pid = local_server_process.pid - print(f"Local server started (PID: {local_server_pid}). Log: {generic_server_log_path}") - print("Waiting for server to initialize...") - time.sleep(5) - - # Attempt Serveo first - print(f"Attempting Serveo tunnel for local port {local_server_port}...") - serveo_log_path = os.path.join(log_dir, f"serveo_{evaluator_id}.log") - serveo_tunnel_process, serveo_url = start_serveo_and_get_url(local_server_port, serveo_log_path) - - if serveo_url and serveo_tunnel_process: - print(f"Serveo tunnel established: {serveo_url} (PID: {serveo_tunnel_process.pid}). Log: {serveo_log_path}") - return serveo_url, "serveo", local_server_pid, serveo_tunnel_process.pid - else: - print(f"Serveo tunnel failed. Check log: {serveo_log_path}") - print("Attempting fallback to ngrok...") - - ngrok_log_path = os.path.join(log_dir, f"ngrok_{evaluator_id}.log") - # Assuming ngrok authtoken is pre-configured by the user or via NGROK_AUTHTOKEN env var - ngrok_tunnel_process, ngrok_url = start_ngrok_and_get_url(local_server_port, ngrok_log_path) - - if ngrok_url and ngrok_tunnel_process: - print(f"ngrok tunnel established: {ngrok_url} (PID: {ngrok_tunnel_process.pid}). Log: {ngrok_log_path}") - return ngrok_url, "ngrok", local_server_pid, ngrok_tunnel_process.pid - else: - print(f"ngrok tunnel also failed. Check log: {ngrok_log_path}") - # Both failed, stop the local server we started - if local_server_pid: - stop_process(local_server_pid) - return ( - None, - None, - local_server_pid, - None, - ) # URL, provider, server_pid, tunnel_pid - - -def _deploy_to_gcp_cloud_run(args, current_config, gcp_config_from_yaml): - """Handles the logic for --target gcp-cloud-run up to service deployment.""" - print(f"Starting GCP Cloud Run deployment for evaluator '{args.id}'...") - - # Resolve function_ref (must be from CLI for GCP) - if not args.function_ref: # This check is also in main, but good for helper too - print("Error: --function-ref is required for GCP Cloud Run deployment.") - return None - - # Dynamically import the reward function to get its requirements - inline_requirements_content = None - try: - module_name, func_name = args.function_ref.rsplit(".", 1) - module = importlib.import_module(module_name) - reward_func = getattr(module, func_name) - if hasattr(reward_func, "_reward_function_requirements"): - inline_requirements_content = reward_func._reward_function_requirements - if inline_requirements_content: - print(f"Found inline requirements for {args.function_ref}") - except Exception as e: - print(f"Warning: Could not load reward function {args.function_ref} to check for inline requirements: {e}") - # Continue without inline requirements if loading fails - - # Resolve GCP project_id - gcp_project_id = args.gcp_project - if not gcp_project_id and gcp_config_from_yaml: - gcp_project_id = gcp_config_from_yaml.project_id - if not gcp_project_id: - print("Error: GCP Project ID must be provided via --gcp-project argument or in rewardkit.yaml.") - return None - - # Resolve GCP region - gcp_region = args.gcp_region - if not gcp_region and gcp_config_from_yaml: - gcp_region = gcp_config_from_yaml.region - if not gcp_region: - print("Error: GCP Region must be provided via --gcp-region argument or in rewardkit.yaml.") - return None - - # Resolve GCP AR repo name - gcp_ar_repo_name = args.gcp_ar_repo - if not gcp_ar_repo_name and gcp_config_from_yaml: - gcp_ar_repo_name = gcp_config_from_yaml.artifact_registry_repository - if not gcp_ar_repo_name: - gcp_ar_repo_name = "eval-protocol-evaluators" - - print(f"Using GCP Project: {gcp_project_id}, Region: {gcp_region}, AR Repo: {gcp_ar_repo_name}") - - if not ensure_artifact_registry_repo_exists( - project_id=gcp_project_id, region=gcp_region, repo_name=gcp_ar_repo_name - ): - print(f"Failed to ensure Artifact Registry repository '{gcp_ar_repo_name}' exists. Aborting.") - return None - - dockerfile_content = generate_dockerfile_content( - function_ref=args.function_ref, - python_version=( - f"{args.runtime[6]}.{args.runtime[7:]}" - if args.runtime.startswith("python") and len(args.runtime) > 7 - else args.runtime.replace("python", "") - ), - eval_protocol_install_source=".", - user_requirements_path=None, # Explicitly None, inline_requirements_content will be used - inline_requirements_content=inline_requirements_content, - service_port=8080, - ) - if not dockerfile_content: - print("Failed to generate Dockerfile content. Aborting.") - return None - - image_tag = "latest" - image_name_tag = f"{gcp_region}-docker.pkg.dev/{gcp_project_id}/{gcp_ar_repo_name}/{args.id}:{image_tag}" - build_context_dir = os.getcwd() - - if not build_and_push_docker_image( - image_name_tag=image_name_tag, - dockerfile_content=dockerfile_content, - build_context_dir=build_context_dir, - gcp_project_id=gcp_project_id, - ): - print(f"Failed to build and push Docker image {image_name_tag}. Aborting.") - return None - print(f"Successfully built and pushed Docker image: {image_name_tag}") - - gcp_env_vars: Dict[str, str] = {} - parsed_gcp_secrets: Dict[str, Any] = {} - allow_unauthenticated_gcp = True - - resolved_auth_mode = "api-key" - if gcp_config_from_yaml and gcp_config_from_yaml.default_auth_mode: - resolved_auth_mode = gcp_config_from_yaml.default_auth_mode - if args.gcp_auth_mode is not None: - resolved_auth_mode = args.gcp_auth_mode - print(f"Using GCP Auth Mode for service: {resolved_auth_mode}") - - if resolved_auth_mode == "api-key": - print("Configuring GCP Cloud Run service for API key authentication (application layer).") - evaluator_id = args.id - api_key_for_service = None # This is the key the service itself will use - config_path = global_loaded_config_path - - if current_config.evaluator_endpoint_keys and evaluator_id in current_config.evaluator_endpoint_keys: - api_key_for_service = current_config.evaluator_endpoint_keys[evaluator_id] - print(f"Using existing API key for '{evaluator_id}' from configuration for the service.") - else: - api_key_for_service = secrets.token_hex(32) - print(f"Generated new API key for '{evaluator_id}' for the service.") - if not current_config.evaluator_endpoint_keys: - current_config.evaluator_endpoint_keys = {} - current_config.evaluator_endpoint_keys[evaluator_id] = api_key_for_service - if config_path: - _save_config(current_config, config_path) - else: - print(f"Warning: No rewardkit.yaml found to save API key for '{evaluator_id}'.") - - gcp_sanitized_eval_id = "".join(filter(lambda char: char.isalnum() or char in ["-", "_"], args.id)) - if not gcp_sanitized_eval_id: - gcp_sanitized_eval_id = "evalprotocol-evaluator" - secret_id_for_auth_key = f"rk-eval-{gcp_sanitized_eval_id}-authkey" - secret_labels = {"managed-by": "eval-protocol", "evaluator-id": evaluator_id} - - api_key_secret_version_id = ensure_gcp_secret( - project_id=gcp_project_id, - secret_id=secret_id_for_auth_key, - secret_value=api_key_for_service, - labels=secret_labels, - ) - if not api_key_secret_version_id: - print(f"Error: Failed to store API key in GCP Secret Manager for '{evaluator_id}'. Aborting.") - return None - print(f"API key for service stored in GCP Secret Manager: {secret_id_for_auth_key}") - parsed_gcp_secrets["RK_ENDPOINT_API_KEY"] = api_key_secret_version_id - - # Register this key with Fireworks secrets for the shim - fireworks_account_id_for_secret = get_fireworks_account_id() - if fireworks_account_id_for_secret: - fw_eval_id_sanitized = args.id.lower() - fw_eval_id_sanitized = "".join(filter(lambda char: char.isalnum() or char == "-", fw_eval_id_sanitized)) - fw_eval_id_sanitized = "-".join(filter(None, fw_eval_id_sanitized.split("-"))) - if not fw_eval_id_sanitized: - fw_eval_id_sanitized = "evaluator" - fw_eval_id_sanitized = fw_eval_id_sanitized[:40] - fw_secret_key_name = f"rkeval-{fw_eval_id_sanitized}-shim-key" - print(f"Registering API key on Fireworks platform as secret '{fw_secret_key_name}' for shim...") - if create_or_update_fireworks_secret( - account_id=fireworks_account_id_for_secret, - key_name=fw_secret_key_name, - secret_value=api_key_for_service, - ): - print(f"Successfully registered/updated secret '{fw_secret_key_name}' on Fireworks platform.") - else: - print(f"Warning: Failed to register/update secret '{fw_secret_key_name}' on Fireworks platform.") - else: - print("Warning: Fireworks Account ID not found, cannot store shim API key on Fireworks platform.") - - cloud_run_service_url = deploy_to_cloud_run( - service_name=args.id, - image_name_tag=image_name_tag, - gcp_project_id=gcp_project_id, - gcp_region=gcp_region, - allow_unauthenticated=allow_unauthenticated_gcp, # True if api-key mode, app handles auth - env_vars=gcp_env_vars if gcp_env_vars else None, - secrets_to_mount=parsed_gcp_secrets, - ) - - if not cloud_run_service_url: - print("Failed to deploy to Cloud Run or retrieve service URL. Aborting.") - return None - - print(f"Successfully deployed to Cloud Run. Service URL: {cloud_run_service_url}") - return cloud_run_service_url - - -# Helper to save config (can be moved to config.py later) -def _save_config(config_data: RewardKitConfig, path: str): - # Basic save, ideally config.py would provide a robust method - try: - with open(path, "w") as f: - yaml.dump(config_data.model_dump(exclude_none=True), f, sort_keys=False) - print(f"Config updated and saved to {path}") - except Exception as e: - print(f"Warning: Failed to save updated config to {path}: {e}") - - -def deploy_command(args): - """Create and deploy an evaluator or register a remote one.""" - - # Check environment variables - if not check_environment(): - return 1 - - if not args.id: # ID is always required - print("Error: Evaluator ID (--id) is required.") - return 1 - - # Process HuggingFace key mapping if provided - huggingface_message_key_map = None - if args.huggingface_key_map: - try: - huggingface_message_key_map = json.loads(args.huggingface_key_map) - except json.JSONDecodeError: - print("Error: Invalid JSON format for --huggingface-key-map") - return 1 - - # Initialize variables for URL registration path - service_url_to_register = None - # api_key_for_shim = None # Not currently used by create_evaluation for shim auth directly - - # PIDs for cleanup if registration fails for local-serve - local_server_pid_to_clean = None - # serveo_pid_to_clean = None # This was old, replaced by local_tunnel_pid_to_clean - local_tunnel_pid_to_clean = None # Initialize here - - if args.target == "gcp-cloud-run": - current_config = get_config() # Needed by the helper - gcp_config_from_yaml = current_config.gcp_cloud_run if current_config.gcp_cloud_run else None - - cloud_run_service_url = _deploy_to_gcp_cloud_run(args, current_config, gcp_config_from_yaml) - if not cloud_run_service_url: - return 1 # Error already printed by helper - service_url_to_register = cloud_run_service_url - - elif args.target == "local-serve": - # Renamed helper and updated return values - url, tunnel_provider, server_pid, tunnel_pid = _establish_local_server_and_tunnel(args) - if not url: - # _establish_local_server_and_tunnel handles cleanup of server if tunnel fails completely - return 1 # Error already printed by helper - service_url_to_register = url - local_server_pid_to_clean = server_pid - # serveo_pid_to_clean was specific, now it's generic tunnel_pid - # Let's rename it for clarity in the cleanup logic - local_tunnel_pid_to_clean = tunnel_pid - print(f"Tunnel established using {tunnel_provider}.") - - elif args.remote_url: - # This is for --target fireworks (default) but with --remote-url - print(f"Registering remote URL: {args.remote_url} for evaluator '{args.id}'") - if not (args.remote_url.startswith("http://") or args.remote_url.startswith("https://")): - print(f"Error: Invalid --remote-url '{args.remote_url}'. Must start with http:// or https://") - return 1 - if args.metrics_folders: # This check might be redundant if --target is explicit - print("Info: --metrics-folders are ignored when deploying with --remote-url.") - service_url_to_register = args.remote_url - # No specific shim auth provided by this path. - - # Common registration step for targets that produce a URL - if service_url_to_register: - try: - print(f"Registering URL '{service_url_to_register}' with Fireworks AI for evaluator '{args.id}'...") - evaluator = create_evaluation( - evaluator_id=args.id, - remote_url=service_url_to_register, - display_name=args.display_name or args.id, - description=args.description or f"Evaluator for {args.id} at {service_url_to_register}", - force=args.force, - huggingface_dataset=args.huggingface_dataset, - huggingface_split=args.huggingface_split, - huggingface_message_key_map=huggingface_message_key_map, - huggingface_prompt_key=args.huggingface_prompt_key, - huggingface_response_key=args.huggingface_response_key, - # remote_auth_header_name="X-Api-Key" if api_key_for_shim else None, # No API key for shim for now - # remote_auth_header_value=api_key_for_shim # No API key for shim for now - ) - evaluator_name = evaluator.get("name", args.id) - print( - f"Successfully registered evaluator '{evaluator_name}' on Fireworks AI, pointing to '{service_url_to_register}'." - ) - if args.target == "local-serve": - # tunnel_provider is defined in the local-serve block - # We need to ensure it's accessible here or pass it through. - # For now, let's assume tunnel_provider was defined in the calling scope of this block. - # This will require a small adjustment to how tunnel_provider is scoped. - # Let's fetch it from args if we store it there, or pass it. - # Simpler: just make the message generic or re-fetch from the PIDs. - # The variable `tunnel_provider` is set in the `elif args.target == "local-serve":` block. - # It needs to be available here. - # For now, I'll adjust the print statement to be more generic or rely on the PIDs. - # The `tunnel_provider` variable is indeed set in the correct scope. - print( - f"Local server (PID: {local_server_pid_to_clean}) and {tunnel_provider} tunnel (PID: {local_tunnel_pid_to_clean}) are running." - ) - print("They will be stopped automatically when this command exits (e.g., Ctrl+C).") - return 0 - except PlatformAPIError as e: - print(f"Error registering URL with Fireworks AI: {str(e)}") - except Exception as e: - print(f"An unexpected error occurred during Fireworks AI registration: {str(e)}") - finally: - # If registration fails for local-serve, clean up the started processes - if args.target == "local-serve" and ("evaluator" not in locals() or not locals().get("evaluator")): - print("Registration failed or was interrupted for local-serve. Cleaning up local processes...") - if local_tunnel_pid_to_clean: # Use the new generic tunnel PID variable - stop_process(local_tunnel_pid_to_clean) - if local_server_pid_to_clean: - stop_process(local_server_pid_to_clean) - return 1 - - # Fallback to original behavior: Deploying by packaging local metrics_folders (target=fireworks, no remote_url) - # This is when args.target == "fireworks" (default) AND args.remote_url is NOT provided. - elif args.target == "fireworks" and not args.remote_url: - if not args.metrics_folders: - print("Error: --metrics-folders are required for 'fireworks' target if --remote-url is not provided.") - return 1 - for folder_spec in args.metrics_folders: - if "=" not in folder_spec: - print(f"Error: Metric folder format should be 'name=path', got '{folder_spec}'") - return 1 - try: - print(f"Packaging and deploying metrics for evaluator '{args.id}' to Fireworks AI...") - evaluator = create_evaluation( - evaluator_id=args.id, - metric_folders=args.metrics_folders, - display_name=args.display_name or args.id, - description=args.description or f"Evaluator: {args.id}", - force=args.force, - huggingface_dataset=args.huggingface_dataset, - huggingface_split=args.huggingface_split, - huggingface_message_key_map=huggingface_message_key_map, - huggingface_prompt_key=args.huggingface_prompt_key, - huggingface_response_key=args.huggingface_response_key, - ) - evaluator_name = evaluator.get("name", args.id) - print(f"Successfully created/updated evaluator: {evaluator_name}") - return 0 - except PlatformAPIError as e: - print(f"Error creating/updating evaluator '{args.id}': {str(e)}") - return 1 - except Exception as e: - print(f"Error creating/updating evaluator '{args.id}': {str(e)}") - return 1 diff --git a/eval_protocol/cli_commands/deploy_mcp.py b/eval_protocol/cli_commands/deploy_mcp.py deleted file mode 100644 index 34cb6a6f..00000000 --- a/eval_protocol/cli_commands/deploy_mcp.py +++ /dev/null @@ -1,290 +0,0 @@ -""" -CLI command for deploying MCP servers to Google Cloud Run. -""" - -import importlib -import os -import sys -import tempfile -from pathlib import Path -from typing import Dict, Optional - -from eval_protocol.config import ( - GCPCloudRunConfig, - RewardKitConfig, - _config_file_path as global_loaded_config_path, - get_config, -) -from eval_protocol.gcp_tools import ( - build_and_push_docker_image, - deploy_to_cloud_run, - ensure_artifact_registry_repo_exists, -) - -from .common import check_environment - - -def _generate_mcp_dockerfile_content( - mcp_server_module: str, - python_version: str = "3.11", - service_port: int = 8000, - additional_requirements: Optional[str] = None, -) -> str: - """ - Generate Dockerfile content for MCP server deployment. - - Args: - mcp_server_module: The Python module containing the MCP server (e.g., 'frozen_lake_mcp_server') - python_version: Python version to use in the container - service_port: Port the MCP server will listen on - additional_requirements: Additional pip requirements - - Returns: - Dockerfile content as string - """ - - # Base requirements for MCP servers - matching setup.py dependencies - base_requirements = [ - "fastmcp>=0.1.0", - # Core Eval Protocol dependencies from setup.py - "requests>=2.25.0", - "pydantic>=2.0.0", - "dataclasses-json>=0.5.7", - "fastapi>=0.68.0", - "uvicorn>=0.15.0", - "python-dotenv>=0.19.0", - "openai==1.78.1", - "aiosqlite", - "aiohttp", - "mcp>=1.9.2", - "PyYAML>=5.0", - "datasets==3.6.0", - "fsspec==2025.3.0", - "hydra-core>=1.3.2", - "omegaconf>=2.3.0", - "gymnasium>=0.29.0", - "httpx>=0.24.0", - "fireworks-ai>=0.17.19", - ] - - if additional_requirements: - requirements = base_requirements + [req.strip() for req in additional_requirements.split("\n") if req.strip()] - else: - requirements = base_requirements - - # Generate pip install lines with proper escaping - pip_install_lines = [] - for req in requirements[:-1]: - pip_install_lines.append(f" {req} \\") - pip_install_lines.append(f" {requirements[-1]}") - pip_install_section = "\n".join(pip_install_lines) - - dockerfile_content = f"""# Multi-stage build for MCP server deployment -FROM python:{python_version}-slim as builder - -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \\ - build-essential \\ - && rm -rf /var/lib/apt/lists/* - -# Copy requirements and install Python dependencies -RUN pip install --no-cache-dir --upgrade pip - -# Install MCP server requirements -RUN pip install --no-cache-dir \\ -{pip_install_section} - -# Production stage -FROM python:{python_version}-slim - -WORKDIR /app - -# Install runtime dependencies -RUN apt-get update && apt-get install -y \\ - && rm -rf /var/lib/apt/lists/* - -# Copy Python packages from builder -COPY --from=builder /usr/local/lib/python{python_version}/site-packages /usr/local/lib/python{python_version}/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin - -# Copy the MCP server code -COPY . . - -# Set environment variables for Cloud Run -# FastMCP uses HOST and PORT environment variables for streamable-http transport -ENV HOST=0.0.0.0 -ENV PORT={service_port} -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# Expose the port -EXPOSE {service_port} - -# Run the MCP server with proper host and port for Cloud Run -CMD ["python", "-m", "{mcp_server_module}", "--host", "0.0.0.0", "--port", "{service_port}"] -""" - - return dockerfile_content - - -def _deploy_mcp_to_gcp_cloud_run(args, current_config, gcp_config_from_yaml): - """Deploy MCP server to GCP Cloud Run.""" - print(f"Starting MCP server deployment to GCP Cloud Run for '{args.id}'...") - - # Validate required arguments - either dockerfile or mcp-server-module must be provided - if not args.dockerfile and not args.mcp_server_module: - print("Error: Either --dockerfile or --mcp-server-module is required for MCP server deployment.") - return None - - # Resolve GCP configuration - gcp_project_id = args.gcp_project - if not gcp_project_id and gcp_config_from_yaml: - gcp_project_id = gcp_config_from_yaml.project_id - if not gcp_project_id: - print("Error: GCP Project ID must be provided via --gcp-project or rewardkit.yaml.") - return None - - gcp_region = args.gcp_region - if not gcp_region and gcp_config_from_yaml: - gcp_region = gcp_config_from_yaml.region - if not gcp_region: - print("Error: GCP Region must be provided via --gcp-region or rewardkit.yaml.") - return None - - gcp_ar_repo_name = args.gcp_ar_repo - if not gcp_ar_repo_name and gcp_config_from_yaml: - gcp_ar_repo_name = gcp_config_from_yaml.artifact_registry_repository - if not gcp_ar_repo_name: - gcp_ar_repo_name = "eval-protocol-mcp-servers" - - print(f"Using GCP Project: {gcp_project_id}, Region: {gcp_region}, AR Repo: {gcp_ar_repo_name}") - - # Ensure Artifact Registry repository exists - if not ensure_artifact_registry_repo_exists( - project_id=gcp_project_id, region=gcp_region, repo_name=gcp_ar_repo_name - ): - print(f"Failed to ensure Artifact Registry repository '{gcp_ar_repo_name}' exists. Aborting.") - return None - - # Determine Dockerfile content - use provided file or generate - dockerfile_content = None - if hasattr(args, "dockerfile") and args.dockerfile: - # Use provided Dockerfile - dockerfile_path = Path(args.dockerfile) - if not dockerfile_path.exists(): - print(f"Error: Dockerfile not found at {dockerfile_path}") - return None - print(f"Using provided Dockerfile: {dockerfile_path}") - try: - with open(dockerfile_path, "r") as f: - dockerfile_content = f.read() - except Exception as e: - print(f"Error reading Dockerfile at {dockerfile_path}: {e}") - return None - else: - # Generate Dockerfile content (legacy approach) - print("Generating Dockerfile content from mcp-server-module...") - dockerfile_content = _generate_mcp_dockerfile_content( - mcp_server_module=args.mcp_server_module, - python_version=getattr(args, "python_version", "3.11"), - service_port=getattr(args, "port", 8000), - additional_requirements=getattr(args, "requirements", None), - ) - - if not dockerfile_content: - print("Failed to obtain Dockerfile content. Aborting.") - return None - - # Build and push Docker image - image_tag = "latest" - image_name_tag = f"{gcp_region}-docker.pkg.dev/{gcp_project_id}/{gcp_ar_repo_name}/{args.id}:{image_tag}" - build_context_dir = os.getcwd() - - if not build_and_push_docker_image( - image_name_tag=image_name_tag, - dockerfile_content=dockerfile_content, - build_context_dir=build_context_dir, - gcp_project_id=gcp_project_id, - ): - print(f"Failed to build and push Docker image {image_name_tag}. Aborting.") - return None - - print(f"Successfully built and pushed Docker image: {image_name_tag}") - - # Deploy to Cloud Run - service_port = getattr(args, "port", 8000) - env_vars = {} - - # Add any custom environment variables - if hasattr(args, "env_vars") and args.env_vars: - for env_pair in args.env_vars: - if "=" in env_pair: - key, value = env_pair.split("=", 1) - env_vars[key] = value - - cloud_run_service_url = deploy_to_cloud_run( - service_name=args.id, - image_name_tag=image_name_tag, - gcp_project_id=gcp_project_id, - gcp_region=gcp_region, - allow_unauthenticated=True, # MCP servers typically need to be publicly accessible - env_vars=env_vars if env_vars else None, - service_port=service_port, - ) - - if not cloud_run_service_url: - print("Failed to deploy to Cloud Run or retrieve service URL. Aborting.") - return None - - print("🚀 Successfully deployed MCP server to Cloud Run!") - print(f"📍 Service URL: {cloud_run_service_url}") - print(f"🔗 MCP Connection URL: {cloud_run_service_url}") - print(f"📋 Service Name: {args.id}") - deployment_method = ( - "local Dockerfile" if (hasattr(args, "dockerfile") and args.dockerfile) else "auto-generated Dockerfile" - ) - print(f"🐳 Deployment Method: {deployment_method}") - print() - print("🎯 Next steps:") - print(f" 1. Test your MCP server: curl {cloud_run_service_url}/health") - print(f" 2. Connect MCP clients to: {cloud_run_service_url}") - print( - f" 3. Monitor logs: gcloud logging read 'resource.type=cloud_run_revision AND resource.labels.service_name={args.id}' --project {gcp_project_id}" - ) - - return cloud_run_service_url - - -def deploy_mcp_command(args): - """Main entry point for MCP server deployment command.""" - - # Check environment (similar to existing deploy command) - if not check_environment(): - print("Environment check failed. Please resolve the issues above before deploying.") - return False - - try: - # Load configuration - current_config = get_config() - gcp_config_from_yaml: Optional[GCPCloudRunConfig] = None - if current_config and current_config.gcp_cloud_run: - gcp_config_from_yaml = current_config.gcp_cloud_run - - # Deploy to GCP Cloud Run - service_url = _deploy_mcp_to_gcp_cloud_run(args, current_config, gcp_config_from_yaml) - - if service_url: - print(f"✅ MCP server '{args.id}' successfully deployed!") - return True - else: - print(f"❌ Failed to deploy MCP server '{args.id}'") - return False - - except Exception as e: - print(f"Error during MCP server deployment: {e}") - import traceback - - traceback.print_exc() - return False diff --git a/eval_protocol/cli_commands/preview.py b/eval_protocol/cli_commands/preview.py deleted file mode 100644 index ef438496..00000000 --- a/eval_protocol/cli_commands/preview.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -CLI command for previewing an evaluator. -""" - -import json -import sys # For sys.exit -from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, Union - -import requests # For making HTTP requests - -from eval_protocol.evaluation import preview_evaluation - -# Assuming EvaluationRequest is defined in generic_server. -# For loose coupling, it might be better in models.py or a shared types module. -from eval_protocol.generic_server import EvaluationRequest -from eval_protocol.models import EvaluateResult, Message - -# Assuming these helper functions exist or will be created in .common -# If not, their logic for loading samples would need to be integrated here or called differently. -from .common import ( - check_environment, - load_samples_from_file, - load_samples_from_huggingface, -) - - -def preview_command(args): - """Preview an evaluator with sample data""" - - # Check environment variables - if not check_environment(): - return 1 - - # Validate --remote-url and --metrics-folders usage - if args.remote_url and args.metrics_folders: - print("Info: --metrics-folders are ignored when --remote-url is specified.") - - if not args.remote_url and not args.metrics_folders: - print("Error: Either --remote-url or --metrics-folders must be specified.") - return 1 - - # Ensure either samples or huggingface_dataset is provided (still needed for remote_url) - if not args.samples and not args.huggingface_dataset: - print("Error: Either sample file (--samples) or HuggingFace dataset (--huggingface-dataset) is required.") - return 1 - - # If using samples file, verify it exists - if args.samples and not Path(args.samples).exists(): - print(f"Error: Sample file '{args.samples}' not found") - return 1 - - # Process HuggingFace key mapping if provided - huggingface_message_key_map = None - if args.huggingface_key_map: - try: - huggingface_message_key_map = json.loads(args.huggingface_key_map) - except json.JSONDecodeError: - print("Error: Invalid JSON format for --huggingface-key-map") - return 1 - - if args.remote_url: - # Handle previewing against a remote URL - print(f"Previewing against remote URL: {args.remote_url}") - - # Ensure the remote URL is a valid base URL - if not (args.remote_url.startswith("http://") or args.remote_url.startswith("https://")): - print(f"Error: Invalid --remote-url '{args.remote_url}'. Must start with http:// or https://") - return 1 - - evaluate_endpoint = f"{args.remote_url.rstrip('/')}/evaluate" - - samples_iterator: Union[List[Any], Iterator[Dict[str, Any]]] = [] - try: - if args.samples: - # Assuming load_samples_from_file yields dicts with "messages" and optional "ground_truth" - samples_iterator = load_samples_from_file(args.samples, args.max_samples) - elif args.huggingface_dataset: - # Assuming load_samples_from_huggingface yields dicts with "messages" and optional "ground_truth" - samples_iterator = load_samples_from_huggingface( - dataset_name=args.huggingface_dataset, - split=args.huggingface_split, - prompt_key=args.huggingface_prompt_key, - response_key=args.huggingface_response_key, - key_map=huggingface_message_key_map, - max_samples=args.max_samples, - ) - except Exception as e: - print(f"Error loading samples: {e}") - return 1 - - results_count = 0 - for i, sample_data in enumerate(samples_iterator): - # The load_samples_from_* helpers should ideally cap at max_samples, - # but we double-check here. - if i >= args.max_samples: - break - results_count += 1 - - messages_payload = sample_data.get("messages", []) - ground_truth_payload = sample_data.get("ground_truth") - # Allow passing other sample fields as kwargs to the reward function - sample_kwargs = {k: v for k, v in sample_data.items() if k not in ["messages", "ground_truth"]} - - processed_messages = [] - for msg_item in messages_payload: - if isinstance(msg_item, Message): # If helpers return Message objects - processed_messages.append(msg_item.model_dump(exclude_none=True)) - elif isinstance(msg_item, dict): # If helpers return dicts - processed_messages.append(msg_item) - else: - print( - f"Warning: Sample {i + 1} has unexpected message item type: {type(msg_item)}. Skipping this message item." - ) - - try: - request_obj = EvaluationRequest( - messages=processed_messages, - ground_truth=ground_truth_payload, - kwargs=sample_kwargs, - ) - except Exception as e: # Pydantic validation for EvaluationRequest - print(f"\n--- Sample {i + 1} ---") - print(f" Error creating request payload for sample: {e}") - print(f" Sample data: {sample_data}") - print("--- End Sample ---") - continue # Skip to next sample - - print(f"\n--- Sample {i + 1} ---") - - try: - response = requests.post( - evaluate_endpoint, - json=request_obj.model_dump(), # Use model_dump() for Pydantic v2, or .dict() for v1 - timeout=30, - ) - response.raise_for_status() - - result_json = response.json() - evaluate_result = EvaluateResult(**result_json) - - print(f" Score: {evaluate_result.score}") - print(f" Reason: {evaluate_result.reason if evaluate_result.reason else 'N/A'}") - print(f" Is Valid: {evaluate_result.is_score_valid}") - if evaluate_result.metrics: - for k, v_metric in evaluate_result.metrics.items(): - print( - f" Metric '{k}': Score={v_metric.score}, Valid={v_metric.is_score_valid}, Reason={v_metric.reason}" - ) - - except requests.exceptions.RequestException as e: - print(f" Error calling remote URL '{evaluate_endpoint}': {e}") - except json.JSONDecodeError: - print( - f" Error: Invalid JSON response from remote URL. Status: {response.status_code}. Response text: {response.text[:200]}..." - ) - except Exception as e: - print(f" Error processing response from remote URL: {e}") - print("--- End Sample ---") - - if results_count == 0: - print("No samples were processed. Check sample source or loading functions.") - return 0 - - else: - # Original behavior: preview using local metrics_folders - # This path is taken if args.remote_url is None (or empty string) - # We already checked above that if not remote_url, then metrics_folders must be present. - - try: - preview_result = preview_evaluation( - metric_folders=args.metrics_folders, - sample_file=args.samples if args.samples else None, - max_samples=args.max_samples, - huggingface_dataset=args.huggingface_dataset, - huggingface_split=args.huggingface_split, - huggingface_prompt_key=args.huggingface_prompt_key, - huggingface_response_key=args.huggingface_response_key, - huggingface_message_key_map=huggingface_message_key_map, - ) - - preview_result.display() - return 0 - except Exception as e: - print(f"Error previewing evaluator (local mode): {str(e)}") - return 1 diff --git a/eval_protocol/evaluation.py b/eval_protocol/evaluation.py index 6123f15d..ab91e9a3 100644 --- a/eval_protocol/evaluation.py +++ b/eval_protocol/evaluation.py @@ -13,6 +13,8 @@ # For type checking only import datasets +from fireworks import Fireworks +import fireworks import requests from eval_protocol.auth import ( @@ -20,16 +22,12 @@ get_fireworks_api_key, verify_api_key_and_get_account_id, ) -from eval_protocol.common_utils import get_user_agent from eval_protocol.typed_interface import EvaluationMode from eval_protocol.get_pep440_version import get_pep440_version logger = logging.getLogger(__name__) -# Flag to track if the preview API was successfully used -used_preview_api = False - def huggingface_dataset_to_jsonl( dataset_name: str, @@ -124,41 +122,6 @@ def huggingface_dataset_to_jsonl( return output_file -class EvaluatorPreviewResult: - def __init__(self): - self.results = [] - self.total_samples = 0 - self.total_runtime_ms = 0 - - def add_result(self, sample_index, success, score, per_metric_evals): - result_obj = types.SimpleNamespace( - index=sample_index, - success=success, - score=score, - per_metric_evals=per_metric_evals, - ) - self.results.append(result_obj) - - def display(self): - print("Evaluation Preview Results") - print("------------------------") - print(f"Total Samples: {self.total_samples}") - print(f"Total Runtime: {self.total_runtime_ms} ms\n") - print("Individual Results:") - print("------------------") - for i, result_obj in enumerate(self.results): - print(f"Sample {result_obj.index + 1}:") - print(f" Success: {result_obj.success}") - print(f" Score: {result_obj.score}") - if hasattr(result_obj, "per_metric_evals") and isinstance(result_obj.per_metric_evals, dict): - for metric, value in result_obj.per_metric_evals.items(): - print(f" {metric}: {value}") - elif hasattr(result_obj, "per_metric_evals"): - print(f" Per-Metric Evals: {result_obj.per_metric_evals}") - if i < len(self.results) - 1: - print() - - class Evaluator: def __init__( self, @@ -192,13 +155,6 @@ def __init__( if not python_code: raise ValueError("python_code is required in ts_mode_config") self.code_files[file_name] = python_code - # ts_mode implies multiMetrics: true for the payload structure - # but it's distinct from folder-based multi_metrics for loading. - # The original self.multi_metrics flag is for folder loading. - # The payload's multiMetrics field will be set to True if ts_mode_config is active. - # The check for (metric_folders or folder) is not applicable in __init__ and was causing an error. - # If ts_mode_config is active, it takes precedence for code definition. - # The multi_metrics flag passed to __init__ is for folder-based loading if ts_mode_config is not used. def _should_include_file(self, filename: str) -> bool: """Check if a file should be included in the evaluator upload.""" @@ -351,225 +307,6 @@ def load_samples_from_jsonl(self, sample_file, max_samples=5): logger.info(f"Loaded {len(samples)} samples from {sample_file}") return samples - def preview(self, sample_file, max_samples=5): - if not self.remote_url and not self.ts_mode_config and not self.code_files: - raise ValueError("No code files loaded. Load metric folder(s) or provide ts_mode_config/remote_url first.") - - # If not remote and not ts_mode, then main.py check applies to loaded code_files - if not self.remote_url and not self.ts_mode_config: - if "main.py" not in self.code_files and not any(k.endswith("/main.py") for k in self.code_files): - raise ValueError("No main.py found in loaded code files for folder-based evaluation.") - - samples = self.load_samples_from_jsonl(sample_file, max_samples) - if not samples: - raise ValueError(f"No valid samples found in {sample_file}") - - auth_token = self.api_key or get_fireworks_api_key() - account_id = self.account_id or get_fireworks_account_id() - if not account_id and auth_token: - account_id = verify_api_key_and_get_account_id(api_key=auth_token, api_base=self.api_base) - logger.debug(f"Preview using account_id: {account_id}") - - if not account_id or not auth_token: - logger.error("Authentication error: Missing Fireworks Account ID or API Key.") - raise ValueError("Missing Fireworks Account ID or API Key.") - - # Keep multiMetrics/rollupSettings for backward compatibility with tests - payload_multi_metrics = True - payload_rollup_settings = {"skipRollup": True} - - # For preview, evaluator_id might not be as critical for shim's env var name, - # but pass it for consistency. Use display_name as a proxy if no specific ID. - preview_evaluator_id_for_shim = self.display_name or "preview_evaluator" - evaluator_payload_data = { - "displayName": self.display_name or "Preview Evaluator", - "description": self.description or "Preview Evaluator", - "multiMetrics": payload_multi_metrics, - "criteria": self._construct_criteria(criteria_data={}), - "requirements": self._get_combined_requirements(), - "rollupSettings": payload_rollup_settings, - } - - sample_strings = [json.dumps(sample) for sample in samples] - payload = { - "evaluator": evaluator_payload_data, - "sampleData": sample_strings, - "maxSamples": max_samples, - } - - api_base = os.environ.get("FIREWORKS_API_BASE", "https://api.fireworks.ai") - - if "dev.api.fireworks.ai" in api_base and account_id == "fireworks": - account_id = "pyroworks-dev" - - url = f"{api_base}/v1/accounts/{account_id}/evaluators:previewEvaluator" - headers = { - "Authorization": f"Bearer {auth_token}", - "Content-Type": "application/json", - "User-Agent": get_user_agent(), - } - logger.info(f"Previewing evaluator using API endpoint: {url} with account: {account_id}") - logger.debug(f"Preview API Request URL: {url}") - logger.debug(f"Preview API Request Headers: {json.dumps(headers, indent=2)}") - logger.debug(f"Preview API Request Payload: {json.dumps(payload, indent=2)}") - - global used_preview_api - try: - response = requests.post(url, json=payload, headers=headers) - response.raise_for_status() - result = response.json() - used_preview_api = True - preview_result_obj = EvaluatorPreviewResult() - preview_result_obj.total_samples = result.get("totalSamples", len(samples)) - preview_result_obj.total_runtime_ms = int(result.get("totalRuntimeMs", 0)) - sample_results = result.get("results", []) - for i, sample_result_item in enumerate(sample_results): - preview_result_obj.add_result( - sample_index=i, - success=sample_result_item.get("success", False), - score=sample_result_item.get("score", 0.0), - per_metric_evals=sample_result_item.get("perMetricEvals", {}), - ) - return preview_result_obj - except Exception as e: - logger.error(f"Error previewing evaluator: {str(e)}") - if isinstance(e, requests.exceptions.HTTPError) and hasattr(e, "response"): - logger.error(f"Response: {e.response.text}") - used_preview_api = False - logger.warning("Falling back to simulated preview mode") - return self._simulated_preview(samples) - - def _get_combined_requirements(self) -> str: - """Combines requirements from all loaded metrics.""" - all_requirements_set = set() - for metric_data in self.metric_folders.values(): - req_list_or_str = metric_data.get("requirements") - if req_list_or_str: - if isinstance(req_list_or_str, list): - for req_item in req_list_or_str: - if isinstance(req_item, str): - all_requirements_set.add(req_item.strip()) - elif isinstance(req_list_or_str, str): # Fallback if somehow a string is still passed - items = [r.strip() for r in req_list_or_str.splitlines() if r.strip()] - for item in items: - all_requirements_set.add(item) - - # For multi_metrics loaded directly into self.code_files (not via metric_folders) - # This part is more complex as it requires loading the 'main.py' from self.code_files - # if self.multi_metrics and not self.metric_folders and "main.py" in self.code_files: - # We would need a temporary way to load this main.py to get its requirements. - # For now, focusing on metric_folders which is the primary path for --metrics-folders. - # If a multi_metrics folder is loaded via load_multi_metrics_folder, it also needs a similar - # dynamic import logic to fetch requirements from its main 'evaluate' function. - # This part is NOT YET IMPLEMENTED for multi_metrics folders. - - if not all_requirements_set and hasattr(self, "_loaded_multi_metric_requirements_str"): - # Fallback for multi_metrics if requirements were loaded differently (hypothetical) - # This attribute doesn't exist yet, placeholder for future enhancement if needed. - if self._loaded_multi_metric_requirements_str: # type: ignore - requirements_list = [ - r.strip() for r in self._loaded_multi_metric_requirements_str.splitlines() if r.strip() - ] # type: ignore - for req_item in requirements_list: - all_requirements_set.add(req_item) - - logger.info(f"Combined unique requirements: {all_requirements_set}") - return "\n".join(sorted(list(all_requirements_set))) - - def _simulated_preview(self, samples): - preview_result = EvaluatorPreviewResult() - preview_result.total_samples = len(samples) - start_time = time.time() - for i, sample in enumerate(samples): - try: - if "messages" not in sample: - raise ValueError(f"Sample {i + 1} is missing 'messages' field") - _ = sample.get("messages", []) - _ = sample.get("ground_truth", []) - _ = sample.get("tools", []) - _ = { - k: v - for k, v in sample.items() - if k - not in [ - "messages", - "ground_truth", - "tools", - ] - } - - if self.multi_metrics or self.ts_mode_config: # ts_mode also implies a single set of results - per_metric_evals = {"quality": 0.8, "relevance": 0.7, "safety": 0.9} - else: - per_metric_evals = {metric_name: 0.75 for metric_name in self.metric_folders} - - score = sum(per_metric_evals.values()) / len(per_metric_evals) if per_metric_evals else 0.0 - preview_result.add_result( - sample_index=i, - success=True, - score=score, - per_metric_evals=per_metric_evals, - ) - except Exception as e: - logger.error(f"Error processing sample {i + 1}: {str(e)}") - preview_result.add_result( - sample_index=i, - success=False, - score=0.0, - per_metric_evals={"error": str(e)}, - ) - end_time = time.time() - preview_result.total_runtime_ms = max(1, int((end_time - start_time) * 1000)) - return preview_result - - def _build_minimal_criteria(self) -> List[Dict[str, str]]: - """Build minimal criteria (name, type, description) without code snippets.""" - - # Remote URL mode - if self.remote_url: - return [ - { - "name": "remote_eval_proxy", - "type": "CODE_SNIPPETS", - "description": f"Proxies evaluation to remote URL: {self.remote_url}", - } - ] - - # TS mode (direct code snippet) - elif self.ts_mode_config: - criterion_name = self.ts_mode_config.get("criterion_name", "default_code_criterion") - description = self.ts_mode_config.get("description", "Python code execution") - return [ - { - "name": criterion_name, - "type": "CODE_SNIPPETS", - "description": description, - } - ] - - # Multi-metrics mode - elif self.multi_metrics: - return [ - { - "name": "eval", - "type": "CODE_SNIPPETS", - "description": self.description or "Multi-metric evaluation", - } - ] - - # Single metric folders - else: - criteria = [] - for metric_name in self.metric_folders: - criteria.append( - { - "name": metric_name, - "type": "CODE_SNIPPETS", - "description": self.description or f"Evaluation metric: {metric_name}", - } - ) - return criteria - @staticmethod def _parse_ignore_file(ignore_path: str) -> List[str]: """Parse .gitignore or .dockerignore and return patterns.""" @@ -706,94 +443,65 @@ def create(self, evaluator_id, display_name=None, description=None, force=False) logger.error("Authentication error: API credentials appear to be invalid or incomplete.") raise ValueError("Invalid or missing API credentials.") + client = Fireworks(api_key=auth_token, base_url=self.api_base, account_id=account_id) + self.display_name = display_name or evaluator_id self.description = description or f"Evaluator created from {evaluator_id}" - # Keep multiMetrics/rollupSettings for backward compatibility with tests - payload_multi_metrics = True - payload_rollup_settings = {"skipRollup": True} - parent = f"accounts/{account_id}" - try: version_str = get_pep440_version() except Exception: version_str = None - payload_data = { - "parent": parent, - "evaluator": { - "displayName": self.display_name, - "description": self.description, - "multiMetrics": payload_multi_metrics, - "commitHash": version_str, - "criteria": self._build_minimal_criteria(), - "requirements": "", - "rollupSettings": payload_rollup_settings, - }, - "evaluatorId": evaluator_id, - } + # Build evaluator params for SDK + from fireworks.types import evaluator_create_params - # Include optional entry point when provided + evaluator_params: evaluator_create_params.Evaluator = { + "display_name": self.display_name, + "description": self.description, + } + if version_str: + evaluator_params["commit_hash"] = version_str if self.entry_point: - payload_data["evaluator"]["entryPoint"] = self.entry_point + evaluator_params["entry_point"] = self.entry_point logger.info(f"Including entryPoint in payload: {self.entry_point}") - # Debug log the create payload structure (without sample data) + # Debug log the create payload structure try: - logger.info(f"Create API Request Payload: {json.dumps(payload_data, indent=2)}") + logger.info(f"Create API Request: evaluator_id={evaluator_id}, evaluator={evaluator_params}") except Exception: - # If serialization fails for any reason, skip debug dump pass - if "dev.api.fireworks.ai" in self.api_base and account_id == "fireworks": - account_id = "pyroworks-dev" - - base_url = f"{self.api_base}/v1/{parent}/evaluatorsV2" - headers = { - "Authorization": f"Bearer {auth_token}", - "Content-Type": "application/json", - "User-Agent": get_user_agent(), - } - self._ensure_requirements_present(os.getcwd()) logger.info(f"Creating evaluator '{evaluator_id}' for account '{account_id}'...") try: if force: - check_url = f"{self.api_base}/v1/{parent}/evaluators/{evaluator_id}" try: - logger.info(f"Checking if evaluator exists: {check_url}") - check_response = requests.get(check_url, headers=headers) - - if check_response.status_code == 200: + logger.info("Checking if evaluator exists") + existing_evaluator = client.evaluators.get(evaluator_id=evaluator_id) + if existing_evaluator: logger.info(f"Evaluator '{evaluator_id}' already exists, deleting and recreating...") - delete_url = f"{self.api_base}/v1/{parent}/evaluators/{evaluator_id}" try: - delete_response = requests.delete(delete_url, headers=headers) - if delete_response.status_code < 400: - logger.info(f"Successfully deleted evaluator '{evaluator_id}'") - else: - logger.warning( - f"Unable to delete evaluator '{evaluator_id}', status: {delete_response.status_code}" - ) - except Exception as e_del: - logger.warning(f"Error deleting evaluator: {str(e_del)}") - response = requests.post(base_url, json=payload_data, headers=headers) - else: - response = requests.post(base_url, json=payload_data, headers=headers) - except requests.exceptions.RequestException: - response = requests.post(base_url, json=payload_data, headers=headers) - else: - logger.info(f"Creating evaluator at: {base_url}") - response = requests.post(base_url, json=payload_data, headers=headers) - - response.raise_for_status() - result = response.json() + client.evaluators.delete(evaluator_id=evaluator_id) + logger.info(f"Successfully deleted evaluator '{evaluator_id}'") + except fireworks.NotFoundError: + logger.info(f"Evaluator '{evaluator_id}' not found, creating...") + except fireworks.APIError as e: + logger.warning(f"Error deleting evaluator: {str(e)}") + except fireworks.NotFoundError: + logger.info(f"Evaluator '{evaluator_id}' does not exist, creating...") + + # Create evaluator using SDK + result = client.evaluators.create( + evaluator_id=evaluator_id, + evaluator=evaluator_params, + ) logger.info(f"Successfully created evaluator '{evaluator_id}'") # Upload code as tar.gz to GCS - evaluator_name = result.get("name") # e.g., "accounts/pyroworks/evaluators/test-123" + evaluator_name = result.name # e.g., "accounts/pyroworks/evaluators/test-123" if not evaluator_name: raise ValueError( @@ -810,20 +518,18 @@ def create(self, evaluator_id, display_name=None, description=None, force=False) tar_size = self._create_tar_gz_with_ignores(tar_path, cwd) - # Call GetEvaluatorUploadEndpoint - upload_endpoint_url = f"{self.api_base}/v1/{evaluator_name}:getUploadEndpoint" - upload_payload = {"name": evaluator_name, "filename_to_size": {tar_filename: tar_size}} - + # Call GetEvaluatorUploadEndpoint using SDK logger.info(f"Requesting upload endpoint for {tar_filename}") - upload_response = requests.post(upload_endpoint_url, json=upload_payload, headers=headers) - upload_response.raise_for_status() + upload_response = client.evaluators.get_upload_endpoint( + evaluator_id=evaluator_id, + filename_to_size={tar_filename: str(tar_size)}, + ) # Check for signed URLs - upload_response_data = upload_response.json() - signed_urls = upload_response_data.get("filenameToSignedUrls", {}) + signed_urls = upload_response.filename_to_signed_urls or {} if not signed_urls: - raise ValueError(f"GetUploadEndpoint returned no signed URLs. Response: {upload_response_data}") + raise ValueError(f"GetUploadEndpoint returned no signed URLs. Response: {upload_response}") signed_url = signed_urls.get(tar_filename) @@ -894,14 +600,11 @@ def create(self, evaluator_id, display_name=None, description=None, force=False) logger.error(f"Upload failed after {max_retries} attempts") raise - # Step 3: Validate upload - validate_url = f"{self.api_base}/v1/{evaluator_name}:validateUpload" - validate_payload = {"name": evaluator_name} - validate_response = requests.post(validate_url, json=validate_payload, headers=headers) - validate_response.raise_for_status() - - validate_data = validate_response.json() - + # Step 3: Validate upload using SDK + client.evaluators.validate_upload( + evaluator_id=evaluator_id, + body={}, + ) logger.info("Upload validated successfully") # Clean up tar file @@ -913,275 +616,14 @@ def create(self, evaluator_id, display_name=None, description=None, force=False) # Don't fail - evaluator is created, just code upload failed return result # Return after attempting upload + except fireworks.APIStatusError as e: + logger.error(f"Error creating evaluator: {str(e)}") + logger.error(f"Status code: {e.status_code}, Response: {e.response.text}") + raise except Exception as e: logger.error(f"Error creating evaluator: {str(e)}") - if isinstance(e, requests.exceptions.HTTPError) and hasattr(e, "response"): - logger.error(f"Response: {e.response.text}") raise - def _construct_criteria(self, criteria_data: Any) -> Any: - assertions = [] - if self.remote_url: - shim_main_py_content = f""" -import json -import os -import requests - -REMOTE_EVALUATOR_URL = "{self.remote_url}" - -def evaluate(messages, ground_truth: Optional[Union[str, List[Dict[str, Any]]]] = None, tools=None, **kwargs): - payload = {{ - "messages": messages, - "ground_truth": ground_truth, - "tools": tools, - "kwargs": kwargs - }} - headers = {{"Content-Type": "application/json"}} - try: - response = requests.post(REMOTE_EVALUATOR_URL, json=payload, headers=headers, timeout=30) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - error_info = {{ - "error": f"Failed to call remote evaluator at {{REMOTE_EVALUATOR_URL}}: {{str(e)}}", - "status_code": getattr(e.response, 'status_code', None), - "response_text": getattr(e.response, 'text', None) - }} - return {{ - "score": 0.0, "reason": f"Error calling remote evaluator: {{str(e)}}", - "is_score_valid": False, "metrics": {{"remote_call_error": {{"score": 0.0, "is_score_valid": False, "reason": json.dumps(error_info)}}}} - }} - except Exception as e: - return {{ - "score": 0.0, "reason": f"Unexpected error in remote evaluator shim: {{str(e)}}", - "is_score_valid": False, "metrics": {{"shim_error": {{"score": 0.0, "is_score_valid": False, "reason": str(e)}}}} - }} -""" - file_contents = {"main.py": shim_main_py_content} - assertions.append( - { - "codeSnippets": { - "language": "python", - "fileContents": file_contents, - }, - "name": "remote_eval_proxy", - "type": "CODE_SNIPPETS", - "description": f"Proxies evaluation to remote URL: {self.remote_url}", - } - ) - elif self.ts_mode_config: - python_code = self.ts_mode_config.get("python_code") - file_name = self.ts_mode_config.get("file_name", "main.py") - criterion_name = self.ts_mode_config.get("criterion_name", "default_code_criterion") - description = self.ts_mode_config.get("description", "Python code execution") - if not python_code: - raise ValueError("python_code is required in ts_mode_config") - entry_func = "evaluate" - try: - if self.entry_point and "::" in self.entry_point: - entry_func = self.entry_point.split("::", 1)[1] - except Exception: - entry_func = "evaluate" - assertions.append( - { - "type": "CODE_SNIPPETS", - "name": criterion_name, - "description": description, - "codeSnippets": { - "language": "python", - "fileContents": {file_name: python_code}, - "entryFile": file_name, - "entryFunc": entry_func, - }, - } - ) - elif self.multi_metrics: - file_contents = {} - for filename, content in self.code_files.items(): - if filename.endswith(".py"): - file_contents[filename] = self._update_evaluate_signature(content) - elif self._should_include_file(filename) and not filename.endswith(".py"): - file_contents[filename] = content - if not file_contents: - raise ValueError("No files found for multi-metrics mode.") - # Determine entry file from entry_point if provided; otherwise detect - entry_file = None - if self.entry_point and "::" in self.entry_point: - try: - ep_file = self.entry_point.split("::", 1)[0] - if ep_file in file_contents: - entry_file = ep_file - else: - ep_base = os.path.basename(ep_file) - for fname in file_contents.keys(): - if os.path.basename(fname) == ep_base: - entry_file = fname - break - except Exception: - entry_file = None - if not entry_file: - try: - for fname, content in file_contents.items(): - for line in content.split("\n"): - s = line.lstrip() - if s.startswith("def evaluate(") or s.startswith("async def evaluate("): - entry_file = fname - break - if entry_file: - break - except Exception: - entry_file = None - if not entry_file: - entry_file = "main.py" if "main.py" in file_contents else list(file_contents.keys())[0] - entry_func = "evaluate" - try: - if self.entry_point and "::" in self.entry_point: - entry_func = self.entry_point.split("::", 1)[1] - except Exception: - entry_func = "evaluate" - assertions.append( - { - "codeSnippets": { - "language": "python", - "fileContents": file_contents, - "entryFile": entry_file, - "entryFunc": entry_func, - }, - "name": "eval", - "type": "CODE_SNIPPETS", - "description": self.description or "Multi-metric evaluation", - } - ) - else: # Folder-based, non-multi_metrics - for metric_name in self.metric_folders: - file_contents = {} - # Include all discovered files for this metric folder, preserving filenames - for filename, content in self.code_files.items(): - if filename.startswith(f"{metric_name}/"): - # Use the file name within the metric folder for clarity - short_name = filename.split(f"{metric_name}/", 1)[1] - if filename.endswith(".py"): - file_contents[short_name] = self._update_evaluate_signature(content) - elif self._should_include_file(filename) and not filename.endswith(".py"): - file_contents[short_name] = content - if not file_contents: - logger.warning(f"No files prepared for metric '{metric_name}', skipping this metric for criteria.") - continue - # Determine entry file within this metric's files using entry_point if present - entry_file = None - if self.entry_point and "::" in self.entry_point: - try: - ep_file = self.entry_point.split("::", 1)[0] - if ep_file in file_contents: - entry_file = ep_file - else: - ep_base = os.path.basename(ep_file) - for fname in file_contents.keys(): - if os.path.basename(fname) == ep_base: - entry_file = fname - break - except Exception: - entry_file = None - if not entry_file: - try: - for fname, content in file_contents.items(): - for line in content.split("\n"): - s = line.lstrip() - if s.startswith("def evaluate(") or s.startswith("async def evaluate("): - entry_file = fname - break - if entry_file: - break - except Exception: - entry_file = None - if not entry_file: - entry_file = "main.py" if "main.py" in file_contents else list(file_contents.keys())[0] - - entry_func = "evaluate" - try: - if self.entry_point and "::" in self.entry_point: - entry_func = self.entry_point.split("::", 1)[1] - except Exception: - entry_func = "evaluate" - assertions.append( - { - "codeSnippets": { - "language": "python", - "fileContents": file_contents, - "entryFile": entry_file, - "entryFunc": entry_func, - }, - "name": metric_name, - "type": "CODE_SNIPPETS", - "description": f"Metric: {metric_name}", - } - ) - - if not assertions: - raise ValueError("No valid criteria could be constructed.") - return assertions - - def _update_evaluate_signature(self, content): - import re - - # Simple regex to match the old evaluate function signature - old_pattern = r"def\s+evaluate\s*\(\s*entry\s*(?::\s*dict)?\s*\)" - # Regex to match the signature we are changing from (original_messages) - current_signature_pattern = ( - r"def\s+evaluate\s*\(\s*messages,\s*original_messages\s*=\s*None,\s*tools\s*=\s*None,\s*\*\*kwargs\s*\)" - ) - new_signature = "def evaluate(messages, ground_truth: Optional[Union[str, List[Dict[str, Any]]]] = None, tools=None, **kwargs)" - - # Check if the old pattern (entry-based) exists - if re.search(old_pattern, content): - updated_content = re.sub(old_pattern, new_signature, content, count=1) - - # Add a compatibility layer for the 'entry' style - compat_layer = """ - # Compatibility layer for old 'entry' format - if ground_truth is None: # Default ground_truth from messages if not provided - ground_truth = messages - # Assuming 'entry' dict was constructed from messages, original_messages (now ground_truth), tools, kwargs - # This part might need more context on how 'entry' was used. - # For now, we'll assume ground_truth takes precedence or is derived. -""" - # Check if the current signature (with original_messages) exists - elif re.search(current_signature_pattern, content): - updated_content = re.sub(current_signature_pattern, new_signature, content, count=1) - # No specific compatibility layer needed here as it's a direct parameter rename - compat_layer = "" # No additional layer for this direct change - else: - # If neither known signature is found, return content as is - return content - - # Find the function body indent level if a change was made - if "updated_content" in locals() and compat_layer: # Only add layer if it's defined - func_match = re.search(r"def\s+evaluate.*?:\s*\n(\s+)", updated_content, re.DOTALL) - if func_match: - indent = func_match.group(1) - # Adjust indentation of compatibility layer - indented_compat_layer = "\n".join(indent + line for line in compat_layer.strip().split("\n")) - - # Insert compatibility layer after function definition - updated_content = re.sub( - re.escape(new_signature) + r"\s*:", - new_signature + ":" + indented_compat_layer, - updated_content, - count=1, - ) - return updated_content - elif "updated_content" in locals(): - return updated_content - return content - - def _get_combined_code(self): # This method seems unused now, consider removal - # ... (implementation unchanged, but likely dead code) - pass - - def _get_code_from_files(self, files): # This method seems unused now, consider removal - # ... (implementation unchanged, but likely dead code) - pass - def _get_authentication(self): account_id = get_fireworks_account_id() auth_token = get_fireworks_api_key() @@ -1195,130 +637,6 @@ def _get_authentication(self): # Helper functions for CLI commands -def preview_evaluation( - metric_folders: Optional[List[str]] = None, - multi_metrics: bool = False, - folder: Optional[str] = None, - python_code_to_evaluate: Optional[str] = None, - python_file_name_for_code: str = "main.py", - criterion_name_for_code: str = "default_code_criterion", - criterion_description_for_code: str = "Python code execution", - sample_file: Optional[str] = None, - max_samples: int = 5, - huggingface_dataset: Optional[str] = None, - huggingface_split: str = "train", - huggingface_message_key_map: Optional[Dict[str, str]] = None, - huggingface_response_key: str = "response", - huggingface_prompt_key: str = "prompt", - reward_function_mode: EvaluationMode = "pointwise", # Added for consistency - account_id: Optional[str] = None, - api_key: Optional[str] = None, -): - ts_mode_config = None - if python_code_to_evaluate: - if metric_folders or folder: # Removed multi_metrics from this check as it's handled by Evaluator init - raise ValueError( - "Cannot use python_code_to_evaluate with folder-based parameters (metric_folders, folder)." - ) - ts_mode_config = { - "python_code": python_code_to_evaluate, - "file_name": python_file_name_for_code, - "criterion_name": criterion_name_for_code, - "description": criterion_description_for_code, - } - # When python_code_to_evaluate is used, multi_metrics in Evaluator constructor is effectively True - # due to how ts_mode_config is handled (sets self.multi_metrics = True for payload). - # The multi_metrics flag passed to Evaluator here should be the original one for folder logic. - evaluator = Evaluator( - multi_metrics=multi_metrics, - ts_mode_config=ts_mode_config, - reward_function_mode=reward_function_mode, - account_id=account_id, - api_key=api_key, - ) - else: - evaluator = Evaluator( - multi_metrics=multi_metrics, - reward_function_mode=reward_function_mode, - account_id=account_id, - api_key=api_key, - ) # Pass mode to Evaluator - if multi_metrics: - if not folder: - raise ValueError("`folder` must be specified for multi_metrics mode.") - evaluator.load_multi_metrics_folder(folder) - else: - if not metric_folders: - raise ValueError("At least one metric_folder must be specified.") - for pair in metric_folders: - if "=" not in pair: - raise ValueError(f"Invalid metric-folder format: {pair}.") - metric_name, folder_path = pair.split("=", 1) - evaluator.load_metric_folder(metric_name, folder_path) - - if huggingface_dataset: - if sample_file: - logger.warning("Both sample_file and huggingface_dataset specified. Using HuggingFace dataset.") - sample_file = huggingface_dataset_to_jsonl( - dataset_name=huggingface_dataset, - split=huggingface_split, - max_samples=max_samples, - message_key_map=huggingface_message_key_map, - response_key=huggingface_response_key, - prompt_key=huggingface_prompt_key, - ) - logger.info(f"Converted dataset saved to: {sample_file}") - - if not sample_file: - raise ValueError("Either sample_file or huggingface_dataset must be specified.") - return evaluator.preview(sample_file, max_samples) - - -def preview_folder_evaluation( # This function might become redundant or need to align with the new preview_evaluation - evaluator_folder, - sample_file=None, - max_samples=5, - multi_metrics=False, # original multi_metrics - huggingface_dataset=None, - huggingface_split="train", - huggingface_message_key_map=None, - huggingface_response_key="response", - huggingface_prompt_key="prompt", -): - evaluator_folder = os.path.abspath(evaluator_folder) - if not os.path.exists(evaluator_folder): - raise ValueError(f"Evaluator folder does not exist: {evaluator_folder}") - if not os.path.isdir(evaluator_folder): - raise ValueError(f"Not a directory: {evaluator_folder}") - - has_main_py = os.path.exists(os.path.join(evaluator_folder, "main.py")) - # Auto-detect multi_metrics if not specified by caller - detected_multi_metrics = multi_metrics - if has_main_py and not multi_metrics: - py_files = list(Path(evaluator_folder).glob("*.py")) - if len(py_files) > 1: - logger.info("Auto-detecting multi-metrics mode based on folder structure for preview_folder_evaluation") - detected_multi_metrics = True - - # Call the unified preview_evaluation - # This function doesn't directly support ts_mode_config, so python_code_to_evaluate is None - return preview_evaluation( - metric_folders=( - None if detected_multi_metrics else [f"{os.path.basename(evaluator_folder)}={evaluator_folder}"] - ), # Simplified for now - multi_metrics=detected_multi_metrics, - folder=evaluator_folder if detected_multi_metrics else None, - python_code_to_evaluate=None, # Not applicable for this helper - sample_file=sample_file, - max_samples=max_samples, - huggingface_dataset=huggingface_dataset, - huggingface_split=huggingface_split, - huggingface_message_key_map=huggingface_message_key_map, - huggingface_response_key=huggingface_response_key, - huggingface_prompt_key=huggingface_prompt_key, - ) - - def create_evaluation( evaluator_id: str, metric_folders: Optional[List[str]] = None, @@ -1388,84 +706,6 @@ def create_evaluation( evaluator.load_metric_folder(metric_name, folder_path) if huggingface_dataset: - logger.info(f"HuggingFace dataset specified: {huggingface_dataset} (currently for preview only).") + logger.info(f"HuggingFace dataset specified: {huggingface_dataset}") return evaluator.create(evaluator_id, display_name, description, force) - - -def deploy_folder_evaluation( # This function might become redundant or need to align with the new create_evaluation - evaluator_id, - evaluator_folder, - display_name=None, - description=None, - force=False, - multi_metrics=False, # original multi_metrics - huggingface_dataset=None, - huggingface_split="train", - huggingface_message_key_map=None, - huggingface_response_key="response", - huggingface_prompt_key="prompt", - remote_url: Optional[str] = None, -): - evaluator_folder_abs = os.path.abspath(evaluator_folder) if evaluator_folder else None - - # If remote_url is provided, evaluator_folder is less relevant for code loading - # but might still be used for context/metadata if the function design implies it. - # For now, if remote_url, we don't load from folder. - - python_code_to_evaluate = None # This helper doesn't take direct code string - - if not remote_url and not evaluator_folder_abs: - raise ValueError("evaluator_folder must be specified if not using remote_url.") - - if evaluator_folder_abs: - if not os.path.exists(evaluator_folder_abs): - raise ValueError(f"Evaluator folder does not exist: {evaluator_folder_abs}") - if not os.path.isdir(evaluator_folder_abs): - raise ValueError(f"Not a directory: {evaluator_folder_abs}") - - # Auto-detect multi_metrics if not specified and not remote_url and folder is given - detected_multi_metrics = multi_metrics - folder_for_loading = None - metric_folders_for_loading = None - - if not remote_url and evaluator_folder_abs: - has_main_py = os.path.exists(os.path.join(evaluator_folder_abs, "main.py")) - if has_main_py and not multi_metrics: # If user says not multi_metrics, but main.py is at root - py_files = list(Path(evaluator_folder_abs).glob("*.py")) - if len(py_files) > 1: # Heuristic: if multiple .py files at root with main.py, likely multi-metric - logger.info("Auto-detecting multi-metrics mode for deploy_folder_evaluation.") - detected_multi_metrics = True - - if detected_multi_metrics: - folder_for_loading = evaluator_folder_abs - else: # Prepare metric_folders list - metric_folders_for_loading = [] - if has_main_py: # Single metric in the root folder - metric_folders_for_loading.append(f"{os.path.basename(evaluator_folder_abs)}={evaluator_folder_abs}") - else: # Look for subdirectories - for item in os.listdir(evaluator_folder_abs): - item_path = os.path.join(evaluator_folder_abs, item) - if os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, "main.py")): - metric_folders_for_loading.append(f"{item}={item_path}") - if not metric_folders_for_loading: - raise ValueError( - f"No valid metrics found in {evaluator_folder_abs} for non-multi-metric deployment." - ) - - return create_evaluation( - evaluator_id=evaluator_id, - metric_folders=metric_folders_for_loading, - multi_metrics=detected_multi_metrics, # Use the detected or passed-in multi_metrics - folder=folder_for_loading, - python_code_to_evaluate=python_code_to_evaluate, # None for this helper - display_name=display_name, - description=description, - force=force, - huggingface_dataset=huggingface_dataset, - huggingface_split=huggingface_split, - huggingface_message_key_map=huggingface_message_key_map, - huggingface_response_key=huggingface_response_key, - huggingface_prompt_key=huggingface_prompt_key, - remote_url=remote_url, - ) diff --git a/tests/cli_commands/test_deploy_cmd.py b/tests/cli_commands/test_deploy_cmd.py deleted file mode 100644 index fbd38ae8..00000000 --- a/tests/cli_commands/test_deploy_cmd.py +++ /dev/null @@ -1,507 +0,0 @@ -import json -from unittest.mock import MagicMock, patch - -import pytest - -# Module to be tested -from eval_protocol.cli_commands.deploy import deploy_command -from eval_protocol.platform_api import PlatformAPIError # Import for exception testing - - -# --- Mocking argparse.Namespace to simulate parsed CLI arguments --- -class MockArgs: - def __init__(self, **kwargs): - self.verbose = False - self.id = None - self.metrics_folders = None - self.display_name = None - self.description = None - self.force = False - self.huggingface_dataset = None - self.huggingface_split = "train" - self.huggingface_prompt_key = "prompt" - self.huggingface_response_key = "response" - self.huggingface_key_map = None - self.remote_url = None - # For GCP - self.target = "fireworks" # Default target - self.function_ref = None - self.gcp_project = None - self.gcp_region = None - self.gcp_ar_repo = None - self.service_account = None - self.entry_point = "reward_function" - self.runtime = "python311" - self.gcp_auth_mode = None - self.__dict__.update(kwargs) - - -@pytest.fixture -def mock_check_environment(): - with patch("eval_protocol.cli_commands.deploy.check_environment", return_value=True) as mock_check: - yield mock_check - - -@pytest.fixture -def mock_gcp_tools(): - with ( - patch("eval_protocol.cli_commands.deploy.ensure_artifact_registry_repo_exists") as mock_ensure_repo, - patch("eval_protocol.cli_commands.deploy.generate_dockerfile_content") as mock_gen_dockerfile, - patch("eval_protocol.cli_commands.deploy.build_and_push_docker_image") as mock_build_push, - patch("eval_protocol.cli_commands.deploy.deploy_to_cloud_run") as mock_deploy_run, - patch("eval_protocol.cli_commands.deploy.ensure_gcp_secret") as mock_ensure_gcp_secret, - ): - mock_ensure_repo.return_value = True - mock_gen_dockerfile.return_value = "DOCKERFILE CONTENT" - mock_build_push.return_value = True - mock_deploy_run.return_value = "http://mock-cloud-run-url.com/service" - mock_ensure_gcp_secret.return_value = "projects/test-proj/secrets/mocksecret/versions/1" - yield { - "ensure_repo": mock_ensure_repo, - "gen_dockerfile": mock_gen_dockerfile, - "build_push": mock_build_push, - "deploy_run": mock_deploy_run, - "ensure_gcp_secret": mock_ensure_gcp_secret, - } - - -class TestDeployCommandRemoteUrl: - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_remote_url_success(self, mock_create_evaluation_call, mock_check_environment, capsys): - """Test successful registration of a remote URL via create_evaluation.""" - args = MockArgs( - id="my-remote-eval", - remote_url="http://my-evaluator.com/evaluate", - display_name="My Remote Eval", - description="A cool remote evaluator.", - target="fireworks", # Explicitly set target for this path - ) - mock_create_evaluation_call.return_value = { - "name": args.id, # Simulate platform API returning full name - "id": args.id, # Simulate platform API returning id - } - - return_code = deploy_command(args) - assert return_code == 0 - - mock_create_evaluation_call.assert_called_once_with( - evaluator_id=args.id, - remote_url=args.remote_url, - display_name=args.display_name or args.id, - description=args.description - or f"Evaluator for {args.id} at {args.remote_url}", # Updated description format - force=args.force, - huggingface_dataset=args.huggingface_dataset, - huggingface_split=args.huggingface_split, - huggingface_message_key_map=None, - huggingface_prompt_key=args.huggingface_prompt_key, - huggingface_response_key=args.huggingface_response_key, - ) - - captured = capsys.readouterr() - assert ( - f"Registering remote URL: {args.remote_url} for evaluator '{args.id}'" # Updated initial message - in captured.out - ) - assert ( - f"Successfully registered evaluator '{args.id}' on Fireworks AI, pointing to '{args.remote_url}'." # Updated success message - in captured.out - ) - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_remote_url_with_metrics_folders_warning(self, mock_create_eval, mock_check_environment, capsys): - args = MockArgs( - id="my-remote-eval", - remote_url="http://my-evaluator.com/evaluate", - metrics_folders=["mf=path"], - target="fireworks", # Explicitly set target - ) - mock_create_eval.return_value = {"name": args.id} - deploy_command(args) - captured = capsys.readouterr() - assert ( - "Info: --metrics-folders are ignored when deploying with --remote-url." # Updated "not packaged" to "ignored" - in captured.out - ) - - def test_deploy_remote_url_invalid_url_format(self, mock_check_environment, capsys): - args = MockArgs(id="my-eval", remote_url="ftp://invalid.com", target="fireworks") - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: Invalid --remote-url 'ftp://invalid.com'" in captured.out - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_remote_url_platform_api_error(self, mock_create_eval, mock_check_environment, capsys): - args = MockArgs( - id="my-remote-eval-fail", - remote_url="http://my-evaluator.com/evaluate", - target="fireworks", - ) - # Simulate the full error string from PlatformAPIError's __str__ - error_message = "Platform connection failed (Status: 500, Response: N/A)" - mock_create_eval.side_effect = PlatformAPIError( - "Platform connection failed", status_code=500, response_text="N/A" - ) - - return_code = deploy_command(args) - assert return_code == 1 - - captured = capsys.readouterr() - # Updated error message to match common registration block - assert f"Error registering URL with Fireworks AI: {error_message}" in captured.out - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_remote_url_unexpected_error(self, mock_create_eval, mock_check_environment, capsys): - args = MockArgs( - id="my-remote-eval-generic-fail", - remote_url="http://my-evaluator.com/evaluate", - target="fireworks", - ) - mock_create_eval.side_effect = Exception("Something broke") - - return_code = deploy_command(args) - assert return_code == 1 - - captured = capsys.readouterr() - # Updated error message to match common registration block - assert "An unexpected error occurred during Fireworks AI registration: Something broke" in captured.out - - -class TestDeployCommandLocalMode: # This class tests the "fireworks" target (packaging metrics) - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_local_mode_success( # Renaming to reflect it tests "fireworks" target - self, mock_create_eval, mock_check_environment, capsys - ): - mock_create_eval.return_value = {"name": "my-fireworks-eval"} # Adjusted for clarity - args = MockArgs( - id="my-fireworks-eval", - metrics_folders=["mf=./path"], - display_name="My Fireworks Eval", - description="A packaged one.", - target="fireworks", # Explicitly "fireworks" target - ) - return_code = deploy_command(args) - assert return_code == 0 - expected_hf_message_key_map = None - mock_create_eval.assert_called_once_with( - evaluator_id=args.id, - metric_folders=args.metrics_folders, - display_name=args.display_name or args.id, - description=args.description or f"Evaluator: {args.id}", - force=args.force, - huggingface_dataset=args.huggingface_dataset, - huggingface_split=args.huggingface_split, - huggingface_message_key_map=expected_hf_message_key_map, - huggingface_prompt_key=args.huggingface_prompt_key, - huggingface_response_key=args.huggingface_response_key, - ) - captured = capsys.readouterr() - assert "Packaging and deploying metrics for evaluator 'my-fireworks-eval' to Fireworks AI..." in captured.out - assert "Successfully created/updated evaluator: my-fireworks-eval" in captured.out - - def test_deploy_local_mode_missing_metrics_folders( # Renaming to reflect "fireworks" target - self, mock_check_environment, capsys - ): - args = MockArgs( - id="my-fireworks-eval-fail", target="fireworks", remote_url=None - ) # Explicit target, no remote_url - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - # Updated error message to be specific to "fireworks" target - assert ( - "Error: --metrics-folders are required for 'fireworks' target if --remote-url is not provided." - in captured.out - ) - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_local_mode_create_evaluation_fails( # Renaming - self, mock_create_eval, mock_check_environment, capsys - ): - error_message = "Platform API error (Status: 503, Response: N/A)" - mock_create_eval.side_effect = PlatformAPIError("Platform API error", status_code=503, response_text="N/A") - args = MockArgs(id="my-fireworks-eval", metrics_folders=["mf=./path"], target="fireworks") - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert f"Error creating/updating evaluator 'my-fireworks-eval': {error_message}" in captured.out - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_local_mode_create_evaluation_fails_generic_exception( # Renaming - self, mock_create_eval, mock_check_environment, capsys - ): - mock_create_eval.side_effect = Exception("Generic error") - args = MockArgs(id="my-fireworks-eval", metrics_folders=["mf=./path"], target="fireworks") - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error creating/updating evaluator 'my-fireworks-eval': Generic error" in captured.out - - -class TestDeployCommandGCPMode: - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_gcp_mode_success( - self, - mock_create_evaluation_final_step, - mock_check_environment, - mock_gcp_tools, - capsys, - ): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval-test", - function_ref="my_module.my_func", - gcp_project="test-proj", - gcp_region="us-central1", - gcp_ar_repo="test-repo", - runtime="python310", - gcp_auth_mode="api-key", - ) - mock_create_evaluation_final_step.return_value = {"name": args.id} # Simulate platform API returning full name - - return_code = deploy_command(args) - assert return_code == 0 - - mock_gcp_tools["ensure_repo"].assert_called_once() - mock_gcp_tools["gen_dockerfile"].assert_called_once() - mock_gcp_tools["build_push"].assert_called_once() - mock_gcp_tools["ensure_gcp_secret"].assert_called_once() - mock_gcp_tools["deploy_run"].assert_called_once() - mock_create_evaluation_final_step.assert_called_once() - - captured = capsys.readouterr() - # Check initial message from helper - assert f"Starting GCP Cloud Run deployment for evaluator '{args.id}'..." in captured.out - assert "Successfully built and pushed Docker image" in captured.out - assert ( - f"Successfully deployed to Cloud Run. Service URL: {mock_gcp_tools['deploy_run'].return_value}" - in captured.out - ) - # Check common registration success message - assert ( - f"Successfully registered evaluator '{args.id}' on Fireworks AI, pointing to '{mock_gcp_tools['deploy_run'].return_value}'." - in captured.out - ) - - @patch("eval_protocol.cli_commands.deploy.get_config") - def test_deploy_gcp_mode_missing_args(self, mock_get_config, mock_check_environment, capsys): - # Mock empty config to test missing project/region scenarios - from eval_protocol.config import RewardKitConfig - - mock_get_config.return_value = RewardKitConfig() - - args = MockArgs(target="gcp-cloud-run", id="gcp-eval-incomplete") - # function_ref is missing, gcp_project, gcp_region also - - # Test missing function_ref - temp_args_dict = args.__dict__.copy() - temp_args_dict.pop("function_ref", None) - current_args = MockArgs(**temp_args_dict) - return_code = deploy_command(current_args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: --function-ref is required for GCP Cloud Run deployment." in captured.out - - # Test missing gcp_project - temp_args_dict = args.__dict__.copy() - temp_args_dict["function_ref"] = "a.b" - temp_args_dict.pop("gcp_project", None) - current_args = MockArgs(**temp_args_dict) - return_code = deploy_command(current_args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: GCP Project ID must be provided" in captured.out - - # Test missing gcp_region - temp_args_dict["gcp_project"] = "proj" - temp_args_dict.pop("gcp_region", None) - current_args = MockArgs(**temp_args_dict) - return_code = deploy_command(current_args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: GCP Region must be provided" in captured.out - - @patch( - "eval_protocol.cli_commands.deploy.ensure_artifact_registry_repo_exists", - return_value=False, - ) - def test_deploy_gcp_mode_ensure_repo_fails(self, mock_ensure_repo_fails, mock_check_environment, capsys): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval", - function_ref="a.b", - gcp_project="p", - gcp_region="r", - gcp_ar_repo="repo", - ) - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Failed to ensure Artifact Registry repository" in captured.out - - @patch( - "eval_protocol.cli_commands.deploy.ensure_artifact_registry_repo_exists", - return_value=True, - ) - @patch( - "eval_protocol.cli_commands.deploy.generate_dockerfile_content", - return_value=None, - ) - def test_deploy_gcp_mode_gen_dockerfile_fails( - self, - mock_gen_dockerfile_fails, - mock_ensure_repo, - mock_check_environment, - capsys, - ): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval", - function_ref="a.b", - gcp_project="p", - gcp_region="r", - gcp_ar_repo="repo", - ) - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Failed to generate Dockerfile content. Aborting." in captured.out - - @patch( - "eval_protocol.cli_commands.deploy.ensure_artifact_registry_repo_exists", - return_value=True, - ) - @patch( - "eval_protocol.cli_commands.deploy.generate_dockerfile_content", - return_value="Dockerfile", - ) - @patch( - "eval_protocol.cli_commands.deploy.build_and_push_docker_image", - return_value=False, - ) - def test_deploy_gcp_mode_build_fails( - self, - mock_build_fails, - mock_gen_dockerfile, - mock_ensure_repo, - mock_check_environment, - capsys, - ): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval", - function_ref="a.b", - gcp_project="p", - gcp_region="r", - gcp_ar_repo="repo", - ) - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Failed to build and push Docker image" in captured.out - - @patch( - "eval_protocol.cli_commands.deploy.ensure_artifact_registry_repo_exists", - return_value=True, - ) - @patch( - "eval_protocol.cli_commands.deploy.generate_dockerfile_content", - return_value="Dockerfile", - ) - @patch( - "eval_protocol.cli_commands.deploy.build_and_push_docker_image", - return_value=True, - ) - @patch("eval_protocol.cli_commands.deploy.deploy_to_cloud_run", return_value=None) - @patch( - "eval_protocol.cli_commands.deploy.ensure_gcp_secret", - return_value="projects/p/secrets/mocksecret/versions/1", - ) - def test_deploy_gcp_mode_cloud_run_deploy_fails( - self, - mock_ensure_gcp_secret_individual, - mock_deploy_run_fails, - mock_build_push, - mock_gen_dockerfile, - mock_ensure_repo, - mock_check_environment, - capsys, - ): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval", - function_ref="a.b", - gcp_project="p", - gcp_region="r", - gcp_ar_repo="repo", - gcp_auth_mode="api-key", - ) - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Failed to deploy to Cloud Run or retrieve service URL. Aborting." in captured.out - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - @patch( - "eval_protocol.cli_commands.deploy.ensure_gcp_secret", - return_value="projects/p/secrets/mocksecret/versions/1", - ) - def test_deploy_gcp_mode_final_registration_fails_platform_error( - self, - mock_ensure_gcp_secret_individual, - mock_create_evaluation_final_step, - mock_check_environment, - mock_gcp_tools, - capsys, - ): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval-reg-fail", - function_ref="a.b", - gcp_project="p", - gcp_region="r", - gcp_ar_repo="repo", - gcp_auth_mode="api-key", - ) - error_message = "Registration failed (Status: 400, Response: N/A)" - mock_create_evaluation_final_step.side_effect = PlatformAPIError( - "Registration failed", status_code=400, response_text="N/A" - ) - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - # Updated error message to match common registration block - assert f"Error registering URL with Fireworks AI: {error_message}" in captured.out - - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - @patch( - "eval_protocol.cli_commands.deploy.ensure_gcp_secret", - return_value="projects/p/secrets/mocksecret/versions/1", - ) - def test_deploy_gcp_mode_final_registration_fails_generic_error( - self, - mock_ensure_gcp_secret_individual, - mock_create_evaluation_final_step, - mock_check_environment, - mock_gcp_tools, - capsys, - ): - args = MockArgs( - target="gcp-cloud-run", - id="gcp-eval-reg-fail-gen", - function_ref="a.b", - gcp_project="p", - gcp_region="r", - gcp_ar_repo="repo", - gcp_auth_mode="api-key", - ) - mock_create_evaluation_final_step.side_effect = Exception("Unexpected registration issue") - return_code = deploy_command(args) - assert return_code == 1 - captured = capsys.readouterr() - # Updated error message to match common registration block - assert ( - "An unexpected error occurred during Fireworks AI registration: Unexpected registration issue" - in captured.out - ) diff --git a/tests/cli_commands/test_preview_cmd.py b/tests/cli_commands/test_preview_cmd.py deleted file mode 100644 index 20cf0416..00000000 --- a/tests/cli_commands/test_preview_cmd.py +++ /dev/null @@ -1,218 +0,0 @@ -import json -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest -import requests - -from eval_protocol.cli_commands import preview as preview_cmd_module -from eval_protocol.cli_commands.preview import preview_command -from eval_protocol.generic_server import EvaluationRequest -from eval_protocol.models import EvaluateResult, Message, MetricResult - -try: - from datasets import Dataset, DatasetDict, IterableDataset, IterableDatasetDict - - DATASETS_AVAILABLE = True -except ImportError: - DATASETS_AVAILABLE = False - - class _DummyDSType: - pass - - IterableDataset = _DummyDSType - - -class MockArgs: - def __init__(self, **kwargs): - self.verbose = False - self.metrics_folders = None - self.samples = None - self.max_samples = 5 - self.huggingface_dataset = None - self.huggingface_split = "train" - self.huggingface_prompt_key = "prompt" - self.huggingface_response_key = "response" - self.huggingface_key_map = None - self.remote_url = None - self.__dict__.update(kwargs) - - -@pytest.fixture -def mock_check_environment(): - with patch("eval_protocol.cli_commands.preview.check_environment", return_value=True) as mock_check: - yield mock_check - - -def create_temp_jsonl(tmp_path: Path, samples_data: list) -> str: - sample_file_path = tmp_path / "temp_samples.jsonl" - with open(sample_file_path, "w", encoding="utf-8") as f: - for sample in samples_data: - f.write(json.dumps(sample) + "\n") - return str(sample_file_path) - - -class TestPreviewCommandRemoteUrl: - @patch("requests.post") - def test_preview_remote_url_success_with_file(self, mock_post, mock_check_environment, tmp_path, capsys): - mock_response = MagicMock() - mock_response.status_code = 200 - sample_data_for_file = [ - { - "messages": [ - {"role": "user", "content": "User prompt 1"}, - {"role": "assistant", "content": "Assistant response 1"}, - ], - "ground_truth": "GT 1", - "custom_kwarg": "custom_val_1", - } - ] - temp_sample_file = create_temp_jsonl(tmp_path, sample_data_for_file) - - eval_result_payload = EvaluateResult( - score=0.8, - reason="Remote success", - is_score_valid=True, - metrics={ - "accuracy": MetricResult(score=0.9, reason="High acc", is_score_valid=True) - }, # This already has metrics - ).model_dump() - mock_response.json.return_value = eval_result_payload - mock_post.return_value = mock_response - - args = MockArgs( - remote_url="http://fake-remote-eval.com", - samples=temp_sample_file, - max_samples=1, - ) - return_code = preview_command(args) - assert return_code == 0 - - expected_endpoint = "http://fake-remote-eval.com/evaluate" - expected_payload_sample1 = EvaluationRequest( - messages=sample_data_for_file[0]["messages"], - ground_truth=sample_data_for_file[0]["ground_truth"], - kwargs={"custom_kwarg": sample_data_for_file[0]["custom_kwarg"]}, - ).model_dump() - mock_post.assert_called_once_with(expected_endpoint, json=expected_payload_sample1, timeout=30) - - captured = capsys.readouterr() - assert "Previewing against remote URL: http://fake-remote-eval.com" in captured.out - assert "--- Sample 1 ---" in captured.out - assert "Score: 0.8" in captured.out - - @pytest.mark.skipif(not DATASETS_AVAILABLE, reason="datasets library not installed") - @patch("datasets.load_dataset") - @patch("requests.post") - def test_preview_remote_url_success_with_hf(self, mock_post, mock_hf_load_dataset, mock_check_environment, capsys): - hf_sample_data = [ - { - "prompt": "HF User prompt", - "response": "HF Assistant response", - "ground_truth_col": "HF GT", - } - ] - mock_iterable_ds = MagicMock(spec=IterableDataset) - mock_iterable_ds.__iter__.return_value = iter(hf_sample_data) - mock_hf_load_dataset.return_value = mock_iterable_ds - - mock_response = MagicMock() - mock_response.status_code = 200 - # Corrected: Explicitly provide metrics={} - eval_result_payload = EvaluateResult(score=0.7, reason="HF Remote success", metrics={}).model_dump() - mock_response.json.return_value = eval_result_payload - mock_post.return_value = mock_response - - args = MockArgs( - remote_url="http://fake-hf-eval.com", - huggingface_dataset="test/hf-dataset", - huggingface_prompt_key="prompt", - huggingface_response_key="response", - huggingface_key_map=json.dumps({"ground_truth_col": "ground_truth"}), - max_samples=1, - ) - return_code = preview_command(args) - assert return_code == 0 - - expected_payload = EvaluationRequest( - messages=[ - {"role": "user", "content": "HF User prompt"}, - {"role": "assistant", "content": "HF Assistant response"}, - ], - ground_truth="HF GT", - kwargs={}, - ).model_dump() - mock_post.assert_called_once_with("http://fake-hf-eval.com/evaluate", json=expected_payload, timeout=30) - captured = capsys.readouterr() - assert "Score: 0.7" in captured.out - - @patch("requests.post") - def test_preview_remote_url_http_error(self, mock_post, mock_check_environment, tmp_path, capsys): - sample_data = [{"messages": [{"role": "user", "content": "Test"}]}] - temp_sample_file = create_temp_jsonl(tmp_path, sample_data) - mock_post.side_effect = requests.exceptions.HTTPError("403 Client Error: Forbidden for url") - - args = MockArgs( - remote_url="http://fake-remote-eval.com", - samples=temp_sample_file, - max_samples=1, - ) - return_code = preview_command(args) - assert return_code == 0 - - captured = capsys.readouterr() - assert "Error calling remote URL" in captured.out - assert "403 Client Error: Forbidden for url" in captured.out - - def test_preview_remote_url_invalid_url_format(self, mock_check_environment, tmp_path, capsys): - sample_data = [{"messages": [{"role": "user", "content": "Test"}]}] - temp_sample_file = create_temp_jsonl(tmp_path, sample_data) - args = MockArgs(remote_url="ftp://invalid-url.com", samples=temp_sample_file) - return_code = preview_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: Invalid --remote-url 'ftp://invalid-url.com'" in captured.out - - def test_preview_remote_url_no_samples_provided(self, mock_check_environment, capsys): - args = MockArgs(remote_url="http://fake-remote-eval.com") - return_code = preview_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert ( - "Error: Either sample file (--samples) or HuggingFace dataset (--huggingface-dataset) is required." - in captured.out - ) - - def test_preview_remote_url_sample_file_not_found(self, mock_check_environment, capsys): - args = MockArgs(remote_url="http://fake-remote-eval.com", samples="non_existent.jsonl") - return_code = preview_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: Sample file 'non_existent.jsonl' not found" in captured.out - - -class TestPreviewCommandLocalMode: - @patch("eval_protocol.cli_commands.preview.preview_evaluation") - def test_preview_local_mode_success(self, mock_preview_eval, mock_check_environment, tmp_path, capsys): - sample_data = [{"messages": [{"role": "user", "content": "Test"}]}] - temp_sample_file = create_temp_jsonl(tmp_path, sample_data) - - mock_preview_result = MagicMock() - mock_preview_eval.return_value = mock_preview_result - - args = MockArgs(metrics_folders=["mf=path"], samples=temp_sample_file) - return_code = preview_command(args) - - assert return_code == 0 - mock_preview_eval.assert_called_once() - mock_preview_result.display.assert_called_once() - - def test_preview_local_mode_missing_metrics_folders(self, mock_check_environment, tmp_path, capsys): - sample_data = [{"messages": [{"role": "user", "content": "Test"}]}] - temp_sample_file = create_temp_jsonl(tmp_path, sample_data) - args = MockArgs(samples=temp_sample_file) - - return_code = preview_command(args) - assert return_code == 1 - captured = capsys.readouterr() - assert "Error: Either --remote-url or --metrics-folders must be specified." in captured.out diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 050b98d6..00000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,171 +0,0 @@ -import argparse -import os -import sys -from unittest.mock import MagicMock, patch - -import pytest - -from eval_protocol.cli import deploy_command, main, parse_args, preview_command - - -class TestCLI: - """Tests for the CLI functionality.""" - - @pytest.mark.skip(reason="preview and deploy commands are currently disabled in cli.py") - def test_parse_args(self): - """Test the argument parser.""" - # Test preview command - # Note: This test is less comprehensive than tests/test_cli_args.py - # It doesn't check for --remote-url here. - args, _ = parse_args( # Unpack tuple - ["preview", "--samples", "test.jsonl", "--metrics-folders", "m=p"] - ) # Added metrics folders to pass new check - assert args.command == "preview" - assert args.samples == "test.jsonl" - assert args.max_samples == 5 # default value - - # Test deploy command - args, _ = parse_args(["deploy", "--id", "test-eval", "--metrics-folders", "test=./test"]) # Unpack tuple - assert args.command == "deploy" - assert args.id == "test-eval" - assert args.metrics_folders == ["test=./test"] - assert not args.force # default value - - @patch("eval_protocol.cli_commands.preview.check_environment", return_value=True) - @patch("eval_protocol.cli_commands.preview.preview_evaluation") - def test_preview_command(self, mock_preview_eval, mock_preview_check_env): - """Test the preview command (local mode).""" - mock_preview_result = MagicMock() - mock_preview_result.display = MagicMock() - mock_preview_eval.return_value = mock_preview_result - - args = argparse.Namespace() - args.metrics_folders = ["test=./test"] - args.samples = "test.jsonl" - args.max_samples = 5 - args.huggingface_dataset = None - args.huggingface_split = "train" - args.huggingface_prompt_key = "prompt" - args.huggingface_response_key = "response" - args.huggingface_key_map = None - args.remote_url = None # Added for compatibility with updated preview_command - - with patch("eval_protocol.cli_commands.preview.Path.exists", return_value=True): - result = preview_command(args) - - assert result == 0 - mock_preview_check_env.assert_called_once() - mock_preview_eval.assert_called_once_with( - metric_folders=["test=./test"], - sample_file="test.jsonl", - max_samples=5, - huggingface_dataset=None, - huggingface_split="train", - huggingface_prompt_key="prompt", - huggingface_response_key="response", - huggingface_message_key_map=None, - ) - mock_preview_result.display.assert_called_once() - - @patch("eval_protocol.cli_commands.deploy.check_environment", return_value=True) - @patch("eval_protocol.cli_commands.deploy.create_evaluation") - def test_deploy_command(self, mock_create_eval, mock_deploy_check_env): - """Test the deploy command (local mode).""" - mock_create_eval.return_value = {"name": "test-evaluator"} - - args = argparse.Namespace() - args.metrics_folders = ["test=./test"] - args.id = "test-eval" - args.display_name = "Test Evaluator" - args.description = "Test description" - args.force = True - args.huggingface_dataset = None - args.huggingface_split = "train" - args.huggingface_prompt_key = "prompt" - args.huggingface_response_key = "response" - args.huggingface_key_map = None - args.remote_url = None - - # Add attributes accessed by deploy_command, with defaults for non-GCP target - args.target = "fireworks" # Explicitly set for this local mode test - args.function_ref = None - args.gcp_project = None - args.gcp_region = None - args.gcp_ar_repo = None - args.service_account = None - args.entry_point = "reward_function" # Default from parser - args.runtime = "python311" # Default from parser - args.gcp_auth_mode = None # Default from parser - - # For local deploy, metrics_folders is required. This is checked inside deploy_command. - # The test_parse_args in test_cli_args.py covers parser-level requirement changes. - - result = deploy_command(args) - - assert result == 0 - mock_deploy_check_env.assert_called_once() - mock_create_eval.assert_called_once_with( - evaluator_id="test-eval", - metric_folders=["test=./test"], - display_name="Test Evaluator", - description="Test description", - force=True, - huggingface_dataset=None, - huggingface_split="train", - huggingface_message_key_map=None, # This is derived from args.huggingface_key_map - huggingface_prompt_key="prompt", - huggingface_response_key="response", - # remote_url=None removed as it relies on default - ) - - @patch("eval_protocol.cli_commands.deploy.check_environment", return_value=False) - @patch("eval_protocol.cli_commands.preview.check_environment", return_value=False) - def test_command_environment_check(self, mock_preview_check_env, mock_deploy_check_env): - """Test that commands check the environment and fail if check_environment returns False.""" - preview_args = argparse.Namespace() - # For preview_command to proceed to check_environment, it needs either remote_url or metrics_folders, - # and also sample sources. - preview_args.metrics_folders = ["test=./test"] - preview_args.samples = "test.jsonl" - preview_args.max_samples = 1 - preview_args.huggingface_dataset = None - preview_args.huggingface_split = "train" - preview_args.huggingface_prompt_key = "prompt" - preview_args.huggingface_response_key = "response" - preview_args.huggingface_key_map = None - preview_args.remote_url = None # Added for compatibility - - deploy_args = argparse.Namespace() - deploy_args.id = "test-eval" - # For deploy_command to proceed to check_environment, it needs id. - # If not remote_url, it also needs metrics_folders. - deploy_args.metrics_folders = ["test=./test"] - deploy_args.display_name = None - deploy_args.description = None - deploy_args.force = False - deploy_args.huggingface_dataset = None - deploy_args.huggingface_split = "train" - deploy_args.huggingface_prompt_key = "prompt" - deploy_args.huggingface_response_key = "response" - deploy_args.huggingface_key_map = None - deploy_args.remote_url = None - deploy_args.target = "fireworks" # Ensure target is set - deploy_args.function_ref = None - deploy_args.gcp_project = None - deploy_args.gcp_region = None - deploy_args.gcp_ar_repo = None - deploy_args.service_account = None - deploy_args.entry_point = "reward_function" - deploy_args.runtime = "python311" - deploy_args.gcp_auth_mode = None - - # Mock Path.exists for preview_args if it uses samples file - with patch("eval_protocol.cli_commands.preview.Path.exists", return_value=True): - preview_result = preview_command(preview_args) - - deploy_result = deploy_command(deploy_args) - - assert preview_result == 1 - assert deploy_result == 1 - mock_preview_check_env.assert_called_once() - mock_deploy_check_env.assert_called_once() diff --git a/tests/test_cli_args.py b/tests/test_cli_args.py index 7ba917c4..e0b6c22f 100644 --- a/tests/test_cli_args.py +++ b/tests/test_cli_args.py @@ -1,4 +1,3 @@ -import argparse import subprocess import sys @@ -32,152 +31,13 @@ def test_create_rft_help_does_not_error(): assert "--dry-run" in combined -@pytest.mark.skip(reason="preview and deploy commands are currently disabled in cli.py") -class TestCliArgParsing: - # --- Tests for 'preview' command --- - def test_preview_with_remote_url_and_samples(self): - args_list = [ - "preview", - "--remote-url", - "http://example.com/eval", - "--samples", - "dummy.jsonl", - ] - parsed, _ = parse_args(args_list) - assert parsed.command == "preview" - assert parsed.remote_url == "http://example.com/eval" - assert parsed.samples == "dummy.jsonl" - assert parsed.metrics_folders is None # Should be None if not provided +def test_verbose_flag(): + """Test verbose flag with upload command.""" + parsed_verbose_short, _ = parse_args(["-v", "upload", "--path", "."]) + assert parsed_verbose_short.verbose is True - def test_preview_with_remote_url_and_hf_dataset(self): - args_list = [ - "preview", - "--remote-url", - "http://example.com/eval", - "--hf", - "dataset_name", - ] - parsed, _ = parse_args(args_list) - assert parsed.command == "preview" - assert parsed.remote_url == "http://example.com/eval" - assert parsed.huggingface_dataset == "dataset_name" + parsed_verbose_long, _ = parse_args(["--verbose", "upload", "--path", "."]) + assert parsed_verbose_long.verbose is True - def test_preview_with_remote_url_and_metrics_folders(self): - """Metrics folders should be accepted by argparse but logic in command might ignore/warn.""" - args_list = [ - "preview", - "--remote-url", - "http://example.com/eval", - "--metrics-folders", - "mf=path", - "--samples", - "s.jsonl", - ] - parsed, _ = parse_args(args_list) - assert parsed.command == "preview" - assert parsed.remote_url == "http://example.com/eval" - assert parsed.metrics_folders == ["mf=path"] - - def test_preview_without_remote_url_requires_metrics_folders_or_command_logic_handles( - self, - ): - """Argparse allows no metrics_folders, command logic should enforce if needed.""" - args_list = [ - "preview", - "--samples", - "dummy.jsonl", - ] # No --remote-url, no --metrics-folders - parsed, _ = parse_args(args_list) - assert parsed.command == "preview" - assert parsed.remote_url is None - assert parsed.metrics_folders is None - # The command logic in preview.py now checks: - # if not args.remote_url and not args.metrics_folders: error - - def test_preview_traditional_with_metrics_folders(self): - args_list = [ - "preview", - "--metrics-folders", - "mf=path", - "--samples", - "dummy.jsonl", - ] - parsed, _ = parse_args(args_list) - assert parsed.command == "preview" - assert parsed.metrics_folders == ["mf=path"] - assert parsed.remote_url is None - - # --- Tests for 'deploy' command --- - def test_deploy_with_remote_url(self): - args_list = [ - "deploy", - "--id", - "my-eval", - "--remote-url", - "http://example.com/deploy-eval", - ] - parsed, _ = parse_args(args_list) - assert parsed.command == "deploy" - assert parsed.id == "my-eval" - assert parsed.remote_url == "http://example.com/deploy-eval" - assert parsed.metrics_folders is None # Not required, should be None if not given - - def test_deploy_with_remote_url_and_metrics_folders(self): - """Metrics folders should be accepted by argparse but logic in command might ignore/warn.""" - args_list = [ - "deploy", - "--id", - "my-eval", - "--remote-url", - "http://example.com/eval", - "--metrics-folders", - "mf=path", - ] - parsed, _ = parse_args(args_list) - assert parsed.command == "deploy" - assert parsed.id == "my-eval" - assert parsed.remote_url == "http://example.com/eval" - assert parsed.metrics_folders == ["mf=path"] - - def test_deploy_traditional_without_remote_url(self): - args_list = ["deploy", "--id", "my-eval", "--metrics-folders", "mf=path"] - parsed, _ = parse_args(args_list) - assert parsed.command == "deploy" - assert parsed.id == "my-eval" - assert parsed.metrics_folders == ["mf=path"] - assert parsed.remote_url is None - - def test_deploy_traditional_metrics_folders_still_optional_at_parser_level(self): - """ - --metrics-folders is required=False at parser level. - The command logic in deploy.py enforces it if --remote-url is not present. - """ - args_list = [ - "deploy", - "--id", - "my-eval", - ] # No --metrics-folders, no --remote-url - # This should parse fine, but deploy_command will raise error. - parsed, _ = parse_args(args_list) - assert parsed.command == "deploy" - assert parsed.id == "my-eval" - assert parsed.metrics_folders is None - assert parsed.remote_url is None - - def test_deploy_id_is_required(self): - with pytest.raises(SystemExit): # argparse exits on missing required arg - parse_args(["deploy"]) # Missing --id - - # General verbose flag - def test_verbose_flag(self): - # Global flags like -v or --verbose should typically come before the subcommand - parsed_verbose_short, _ = parse_args(["-v", "preview", "--samples", "s.jsonl", "--metrics-folders", "m=p"]) - assert parsed_verbose_short.verbose is True - - parsed_verbose_long, _ = parse_args( - ["--verbose", "preview", "--samples", "s.jsonl", "--metrics-folders", "m=p"] - ) - assert parsed_verbose_long.verbose is True - - parsed_not_verbose, _ = parse_args(["preview", "--samples", "s.jsonl", "--metrics-folders", "m=p"]) - assert parsed_not_verbose.verbose is False + parsed_not_verbose, _ = parse_args(["upload", "--path", "."]) + assert parsed_not_verbose.verbose is False diff --git a/tests/test_deploy_integration.py b/tests/test_deploy_integration.py deleted file mode 100644 index 1841c9db..00000000 --- a/tests/test_deploy_integration.py +++ /dev/null @@ -1,215 +0,0 @@ -import argparse -import importlib.util -import json -import os -import sys -from pathlib import Path -from unittest.mock import ANY, MagicMock, patch - -import pytest - -from eval_protocol.cli_commands.deploy import deploy_command -from eval_protocol.config import GCPCloudRunConfig, RewardKitConfig - -# Constants for a dummy reward function module -# This module will be created and deleted by tests needing it. -DUMMY_DEPLOY_TEST_MODULE_NAME = "dummy_deploy_test_module" -DUMMY_DEPLOY_TEST_MODULE_FILENAME = f"{DUMMY_DEPLOY_TEST_MODULE_NAME}.py" -DUMMY_DEPLOY_TEST_FUNCTION_NAME = "my_dummy_deploy_reward_func" -DUMMY_DEPLOY_FUNCTION_REF = f"{DUMMY_DEPLOY_TEST_MODULE_NAME}.{DUMMY_DEPLOY_TEST_FUNCTION_NAME}" -DUMMY_DEPLOY_REQUIREMENTS = "requests==2.25.0\nfastapi==0.70.0" - -DUMMY_DEPLOY_MODULE_CONTENT = f""" -from eval_protocol.typed_interface import reward_function - -@reward_function(id="test-deploy-func", requirements='''{DUMMY_DEPLOY_REQUIREMENTS}''') -def {DUMMY_DEPLOY_TEST_FUNCTION_NAME}(messages, ground_truth=None, **kwargs): - return {{"score": 0.5, "reason": "Deployed dummy"}} -""" - -# Ensure the CWD (project root) is in sys.path for module loading during tests -if Path.cwd().as_posix() not in sys.path: - sys.path.insert(0, Path.cwd().as_posix()) - - -@pytest.fixture(scope="function") -def create_dummy_reward_module_for_deploy(): - # Create the dummy module file - with open(DUMMY_DEPLOY_TEST_MODULE_FILENAME, "w") as f: - f.write(DUMMY_DEPLOY_MODULE_CONTENT) - - # Ensure the module can be imported by clearing any cached versions - if DUMMY_DEPLOY_TEST_MODULE_NAME in sys.modules: - del sys.modules[DUMMY_DEPLOY_TEST_MODULE_NAME] - - yield DUMMY_DEPLOY_FUNCTION_REF # Provide the function reference to the test - - # Cleanup: remove the dummy module file - if os.path.exists(DUMMY_DEPLOY_TEST_MODULE_FILENAME): - os.remove(DUMMY_DEPLOY_TEST_MODULE_FILENAME) - # Cleanup: remove from sys.modules if it was loaded - if DUMMY_DEPLOY_TEST_MODULE_NAME in sys.modules: - del sys.modules[DUMMY_DEPLOY_TEST_MODULE_NAME] - - -# Load the deploy_example module directly from the examples folder -def load_module_from_path(name, path): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None: - raise ImportError(f"Could not load spec for module {name} from {path}") - module = importlib.util.module_from_spec(spec) - if spec.loader is None: - raise ImportError(f"Spec for module {name} has no loader") - spec.loader.exec_module(module) - return module - - -@pytest.fixture -def deploy_example(): - # Path to the deploy_example.py file - file_path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - "examples", - "deploy_example.py", - ) - - # Load the module - return load_module_from_path("deploy_example", file_path) - - -@pytest.fixture -def mock_env_variables(monkeypatch): - """Set environment variables for testing""" - monkeypatch.setenv("FIREWORKS_API_KEY", "test_api_key") - monkeypatch.setenv("FIREWORKS_API_BASE", "https://api.fireworks.ai") - # Account id is derived from API key; mock deploy module lookup to keep tests offline. - monkeypatch.setattr("eval_protocol.cli_commands.deploy.get_fireworks_account_id", lambda: "test_account") - - -@pytest.fixture -def mock_requests_post(): - """Mock requests.post method""" - with patch("requests.post") as mock_post: - mock_post.return_value = MagicMock() - mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = { - "name": "accounts/test_account/evaluators/informativeness-v1", - "displayName": "informativeness-v1", - "description": "Evaluates response informativeness based on specificity and content density", - } - yield mock_post - - -@pytest.fixture -def mock_requests_get(): - """Mock requests.get method""" - with patch("requests.get") as mock_get: - mock_get.return_value = MagicMock() - mock_get.return_value.status_code = 404 # Evaluator doesn't exist - yield mock_get - - -def test_deploy_gcp_with_inline_requirements( - mock_env_variables, # Ensures FIREWORKS_API_KEY etc. are set - create_dummy_reward_module_for_deploy, # Creates and cleans up the dummy module -): - """ - Test the deploy_command with --target gcp-cloud-run, ensuring inline requirements - from the @reward_function decorator are passed to generate_dockerfile_content. - """ - function_ref = create_dummy_reward_module_for_deploy - evaluator_id = "test-gcp-evaluator-with-reqs" - - args = argparse.Namespace( - id=evaluator_id, - target="gcp-cloud-run", - function_ref=function_ref, - metrics_folders=None, - remote_url=None, - display_name=None, - description=None, - force=False, - huggingface_dataset=None, - huggingface_split=None, - huggingface_key_map=None, # This is what argparse would create from --huggingface-key-map - huggingface_prompt_key=None, - huggingface_response_key=None, - local_port=8001, # Default, not used for GCP - runtime="python3.10", # Example runtime - gcp_project="test-gcp-project", - gcp_region="us-central1", - gcp_ar_repo=None, # Will default - gcp_auth_mode="api-key", - ) - - # Mock all external dependencies of _deploy_to_gcp_cloud_run and deploy_command - with ( - patch("eval_protocol.cli_commands.deploy.check_environment", return_value=True) as mock_check_env, - patch("eval_protocol.cli_commands.deploy.get_config") as mock_get_config, - patch( - "eval_protocol.cli_commands.deploy.ensure_artifact_registry_repo_exists", - return_value=True, - ) as mock_ensure_ar, - patch( - "eval_protocol.cli_commands.deploy.generate_dockerfile_content", - return_value="DOCKERFILE CONTENT", - ) as mock_gen_dockerfile, - patch( - "eval_protocol.cli_commands.deploy.build_and_push_docker_image", - return_value=True, - ) as mock_build_push, - patch( - "eval_protocol.cli_commands.deploy.ensure_gcp_secret", - return_value="projects/p/secrets/s/versions/1", - ) as mock_ensure_secret, - patch( - "eval_protocol.cli_commands.deploy.create_or_update_fireworks_secret", - return_value=True, - ) as mock_fw_secret, - patch( - "eval_protocol.cli_commands.deploy.deploy_to_cloud_run", - return_value="https://service-url.run.app", - ) as mock_deploy_cr, - patch( - "eval_protocol.cli_commands.deploy.create_evaluation", - return_value={"name": evaluator_id}, - ) as mock_create_eval, - ): - # Configure mock_get_config to return a basic config - mock_config_instance = RewardKitConfig( - gcp_cloud_run=GCPCloudRunConfig( - project_id="test-gcp-project-yaml", # Test CLI override - region="us-west1-yaml", # Test CLI override - default_auth_mode="api-key", - ), - evaluator_endpoint_keys={}, - ) - mock_get_config.return_value = mock_config_instance - - # Call the deploy command - result_code = deploy_command(args) - - assert result_code == 0 - mock_check_env.assert_called_once() - - # Key assertion: generate_dockerfile_content was called with the correct inline_requirements_content - mock_gen_dockerfile.assert_called_once() - call_args, call_kwargs = mock_gen_dockerfile.call_args - assert call_kwargs.get("function_ref") == function_ref - assert call_kwargs.get("inline_requirements_content") == DUMMY_DEPLOY_REQUIREMENTS - assert call_kwargs.get("user_requirements_path") is None # Ensure it's not trying to use both - - mock_ensure_ar.assert_called_once_with( - project_id=args.gcp_project, - region=args.gcp_region, - repo_name="eval-protocol-evaluators", # Default repo name - ) - mock_build_push.assert_called_once() - mock_deploy_cr.assert_called_once() - mock_create_eval.assert_called_once() - - # Check that the dynamically loaded module's requirements were used - # This is implicitly tested by checking mock_gen_dockerfile's call_kwargs - - # Ensure the dummy module is cleaned up by the fixture - # No explicit cleanup needed here due to yield in fixture diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index e251d6d9..c95156a7 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -2,14 +2,11 @@ import os import shutil import tempfile -from pathlib import Path from unittest.mock import MagicMock, patch -import pytest import requests -from eval_protocol.evaluation import Evaluator, create_evaluation, preview_evaluation -from eval_protocol.models import MetricResult +from eval_protocol.evaluation import Evaluator, create_evaluation def create_test_folder(): @@ -103,233 +100,6 @@ def test_evaluator_load_multi_metrics_folder(): shutil.rmtree(tmp_dir, ignore_errors=True) -def test_evaluator_update_evaluate_signature(): - evaluator = Evaluator() - old_code = """ -def evaluate(entry): - messages = entry.get('messages', []) - if not messages: return {'score': 0.0, 'reason': 'No messages found'} - last_message = messages[-1] - content = last_message.get('content', '') - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - return {'score': score, 'reason': f'Word count: {word_count}'} - """ - updated_code = evaluator._update_evaluate_signature(old_code) - assert ( - "def evaluate(messages, ground_truth: Optional[Union[str, List[Dict[str, Any]]]] = None, tools=None, **kwargs)" - in updated_code - ) - # The "entry = {" line is no longer part of the compatibility layer for the old_pattern. - # The compatibility layer now focuses on handling ground_truth. - assert ( - "if ground_truth is None: # Default ground_truth from messages if not provided" in updated_code - ) # Check for new compat layer logic - new_code = """ -def evaluate(messages, ground_truth: Optional[Union[str, List[Dict[str, Any]]]] = None, tools=None, **kwargs): - if not messages: return {'score': 0.0, 'reason': 'No messages found'} - last_message = messages[-1] - content = last_message.get('content', '') - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - return {'score': score, 'reason': f'Word count: {word_count}'} - """ - unchanged_code = evaluator._update_evaluate_signature(new_code) - assert new_code == unchanged_code - - -@patch("eval_protocol.evaluation.requests.post") -def test_evaluator_preview(mock_requests_post, monkeypatch): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = { - "totalSamples": 2, - "totalRuntimeMs": 123, - "results": [ - { - "index": 0, - "success": True, - "score": 0.5, - "reason": "Reason 1", - "perMetricEvals": { - "test_metric": MetricResult(score=0.5, reason="Metric reason 1", is_score_valid=True).model_dump() - }, - }, - { - "index": 1, - "success": True, - "score": 0.8, - "reason": "Reason 2", - "perMetricEvals": { - "test_metric": MetricResult(score=0.8, reason="Metric reason 2", is_score_valid=True).model_dump() - }, - }, - ], - } - mock_requests_post.return_value = mock_response - - monkeypatch.setenv("FIREWORKS_API_KEY", "test_preview_api_key") - monkeypatch.setattr("eval_protocol.evaluation.get_fireworks_account_id", lambda: "test_preview_account") - # Using a mock API base to prevent real calls - monkeypatch.setenv("FIREWORKS_API_BASE", "http://mock-api-server") # Changed to avoid actual localhost call - - # Mock requests.post for the preview call - class MockResponsePreview: - def __init__(self, json_data, status_code=200): - self.json_data = json_data - self.status_code = status_code - self.text = json.dumps(json_data) - - def json(self): - return self.json_data - - def raise_for_status(self): - if self.status_code != 200: - raise requests.exceptions.HTTPError(f"Mock API Error: {self.status_code}") - - def mock_post_preview(*args, **kwargs): - expected_url_preview = "http://mock-api-server/v1/accounts/test_preview_account/evaluators:previewEvaluator" - if args[0] == expected_url_preview: - # Simulate a successful preview API response - return MockResponsePreview( - { - "totalSamples": 2, - "totalRuntimeMs": 150, # Example runtime - "results": [ - { - "success": True, - "score": 0.75, - "perMetricEvals": {"test_metric": 0.75}, - }, - { - "success": True, - "score": 0.85, - "perMetricEvals": {"test_metric": 0.85}, - }, - ], - } - ) - # Fallback for other URLs if any, though not expected in this test - return MockResponsePreview({"error": "Unexpected URL"}, 404) - - monkeypatch.setattr("requests.post", mock_post_preview) - - tmp_dir = create_test_folder() - sample_file = create_sample_file() - try: - evaluator = Evaluator() - evaluator.load_metric_folder("test_metric", tmp_dir) - preview_result = evaluator.preview(sample_file, max_samples=2) - assert preview_result.total_samples == 2 - assert preview_result.total_runtime_ms >= 0 - assert len(preview_result.results) == 2 - assert preview_result.results[0].index == 0 - assert preview_result.results[0].success is True - assert hasattr(preview_result.results[0], "score") - assert preview_result.results[0].score == 0.75 - assert hasattr(preview_result.results[0], "per_metric_evals") # Attribute name in Python object - assert "test_metric" in preview_result.results[0].per_metric_evals - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) - os.unlink(sample_file) - - -@patch("eval_protocol.evaluation.requests.post") -def test_preview_evaluation_helper(mock_requests_post, monkeypatch): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = { - "totalSamples": 2, - "totalRuntimeMs": 100, - "results": [ - { - "index": 0, - "success": True, - "score": 0.6, - "reason": "Helper Reason 1", - "perMetricEvals": { - "test_metric": MetricResult( - score=0.6, reason="Helper Metric reason 1", is_score_valid=True - ).model_dump() - }, - }, - { - "index": 1, - "success": True, - "score": 0.7, - "reason": "Helper Reason 2", - "perMetricEvals": { - "test_metric": MetricResult( - score=0.7, reason="Helper Metric reason 2", is_score_valid=True - ).model_dump() - }, - }, - ], - } - mock_requests_post.return_value = mock_response - - monkeypatch.setenv("FIREWORKS_API_KEY", "test_helper_api_key") - monkeypatch.setattr("eval_protocol.evaluation.get_fireworks_account_id", lambda: "test_helper_account") - # Using a mock API base to prevent real calls - monkeypatch.setenv("FIREWORKS_API_BASE", "http://mock-api-server-helper") # Changed - - # Mock requests.post for the preview_evaluation helper call - class MockResponseHelperPreview: # Renamed to avoid conflict if in same scope, though not strictly necessary here - def __init__(self, json_data, status_code=200): - self.json_data = json_data - self.status_code = status_code - self.text = json.dumps(json_data) - - def json(self): - return self.json_data - - def raise_for_status(self): - if self.status_code != 200: - raise requests.exceptions.HTTPError(f"Mock API Error: {self.status_code}") - - def mock_post_helper_preview(*args, **kwargs): - expected_url_helper_preview = ( - "http://mock-api-server-helper/v1/accounts/test_helper_account/evaluators:previewEvaluator" - ) - if args[0] == expected_url_helper_preview: - return MockResponseHelperPreview( - { - "totalSamples": 2, - "totalRuntimeMs": 160, - "results": [ - { - "success": True, - "score": 0.65, - "perMetricEvals": {"test_metric": 0.65}, - }, - { - "success": True, - "score": 0.70, - "perMetricEvals": {"test_metric": 0.70}, - }, - ], - } - ) - return MockResponseHelperPreview({"error": "Unexpected URL for helper"}, 404) - - monkeypatch.setattr("requests.post", mock_post_helper_preview) - - tmp_dir = create_test_folder() - sample_file = create_sample_file() - try: - preview_result = preview_evaluation( - metric_folders=[f"test_metric={tmp_dir}"], - sample_file=sample_file, - max_samples=2, - ) - assert preview_result.total_samples == 2 - assert len(preview_result.results) == 2 - assert preview_result.results[0].score == 0.65 - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) - os.unlink(sample_file) - - def test_create_evaluation_helper(monkeypatch): tmp_dir = create_test_folder() monkeypatch.setenv("FIREWORKS_API_KEY", "test_api_key") @@ -338,94 +108,81 @@ def test_create_evaluation_helper(monkeypatch): original_cwd = os.getcwd() - class MockResponse: - def __init__(self, json_data, status_code=200): - self.json_data = json_data - self.status_code = status_code - self.text = json.dumps(json_data) - - def json(self): - return self.json_data - - def raise_for_status(self): # pragma: no cover - if self.status_code != 200: - raise Exception("API Error") - + # Track SDK calls create_called = False upload_endpoint_called = False validate_called = False - def mock_post(*args, **kwargs): - nonlocal create_called, upload_endpoint_called, validate_called - url = args[0] - payload = kwargs.get("json", {}) - - # Handle different endpoints in the upload flow - if "getUploadEndpoint" in url: - upload_endpoint_called = True - # Dynamically create signed URLs for whatever filenames are requested - filename_to_size = payload.get("filename_to_size", {}) - signed_urls = {} - for filename in filename_to_size.keys(): - signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" - return MockResponse({"filenameToSignedUrls": signed_urls}) - elif "validateUpload" in url: - validate_called = True - return MockResponse({"success": True, "valid": True}) - else: - # Create evaluator endpoint - create_called = True - assert "evaluator" in payload - assert "evaluatorId" in payload - evaluator_data = payload["evaluator"] - assert "criteria" in evaluator_data - criteria = evaluator_data["criteria"] - assert len(criteria) > 0 - criterion = criteria[0] - assert criterion["type"] == "CODE_SNIPPETS" - # Code is now uploaded as tar.gz, not in criteria - - return MockResponse( - { - "name": "accounts/test_account/evaluators/test-eval", - "displayName": "Test Evaluator", - "description": "Test description", - "multiMetrics": False, - } - ) - - # Mock GCS upload - from unittest.mock import MagicMock - + # Mock the Fireworks SDK client methods + mock_evaluator_result = MagicMock() + mock_evaluator_result.name = "accounts/test_account/evaluators/test-eval" + mock_evaluator_result.display_name = "Test Evaluator" + mock_evaluator_result.description = "Test description" + + def mock_create(evaluator_id, evaluator): + nonlocal create_called + create_called = True + # Verify the evaluator params + assert evaluator_id == "test-eval" + assert "display_name" in evaluator + assert evaluator["display_name"] == "Test Evaluator" + assert "description" in evaluator + assert evaluator["description"] == "Test description" + return mock_evaluator_result + + def mock_get_upload_endpoint(evaluator_id, filename_to_size): + nonlocal upload_endpoint_called + upload_endpoint_called = True + mock_response = MagicMock() + signed_urls = {} + for filename in filename_to_size.keys(): + signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" + mock_response.filename_to_signed_urls = signed_urls + return mock_response + + def mock_validate_upload(evaluator_id, body): + nonlocal validate_called + validate_called = True + return MagicMock() + + # Mock GCS upload (still uses requests.Session) mock_session = MagicMock() mock_gcs_response = MagicMock() mock_gcs_response.status_code = 200 mock_gcs_response.raise_for_status = MagicMock() mock_session.send.return_value = mock_gcs_response - monkeypatch.setattr("requests.post", mock_post) - monkeypatch.setattr("requests.Session", lambda: mock_session) - - try: - os.chdir(tmp_dir) - api_response = create_evaluation( - evaluator_id="test-eval", - metric_folders=[f"test_metric={tmp_dir}"], - display_name="Test Evaluator", - description="Test description", - ) + # Patch the Fireworks client + with patch("eval_protocol.evaluation.Fireworks") as mock_fireworks_class: + mock_client = MagicMock() + mock_fireworks_class.return_value = mock_client + mock_client.evaluators.create = mock_create + mock_client.evaluators.get_upload_endpoint = mock_get_upload_endpoint + mock_client.evaluators.validate_upload = mock_validate_upload + + # Patch requests.Session for GCS upload + monkeypatch.setattr("requests.Session", lambda: mock_session) + + try: + os.chdir(tmp_dir) + api_response = create_evaluation( + evaluator_id="test-eval", + metric_folders=[f"test_metric={tmp_dir}"], + display_name="Test Evaluator", + description="Test description", + ) - # Verify response - assert api_response["name"] == "accounts/test_account/evaluators/test-eval" - assert api_response["displayName"] == "Test Evaluator" - assert api_response["description"] == "Test description" + # Verify response (SDK returns an object, not dict) + assert api_response.name == "accounts/test_account/evaluators/test-eval" + assert api_response.display_name == "Test Evaluator" + assert api_response.description == "Test description" - # Verify full upload flow was executed - assert create_called, "Create endpoint should be called" - assert upload_endpoint_called, "GetUploadEndpoint should be called" - assert validate_called, "ValidateUpload should be called" - assert mock_session.send.called, "GCS upload should happen" + # Verify full upload flow was executed + assert create_called, "Create endpoint should be called" + assert upload_endpoint_called, "GetUploadEndpoint should be called" + assert validate_called, "ValidateUpload should be called" + assert mock_session.send.called, "GCS upload should happen" - finally: - os.chdir(original_cwd) - shutil.rmtree(tmp_dir, ignore_errors=True) + finally: + os.chdir(original_cwd) + shutil.rmtree(tmp_dir, ignore_errors=True) diff --git a/tests/test_evaluation_integration.py b/tests/test_evaluation_integration.py deleted file mode 100644 index de97000b..00000000 --- a/tests/test_evaluation_integration.py +++ /dev/null @@ -1,365 +0,0 @@ -import json -import os -import shutil -import tempfile -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest - -from eval_protocol.evaluation import Evaluator, create_evaluation, preview_evaluation - - -def create_test_folder(): - """Create a temporary folder with a main.py file for testing""" - tmp_dir = tempfile.mkdtemp() - with open(os.path.join(tmp_dir, "main.py"), "w") as f: - f.write( - """ -def evaluate(messages, ground_truth=None, tools=None, **kwargs): # Changed original_messages to ground_truth - if not messages: - return {'score': 0.0, 'reason': 'No messages found'} - last_message = messages[-1] - content = last_message.get('content', '') - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - return { - 'score': score, - 'reason': f'Word count: {word_count}' - } -""" - ) - # Create requirements.txt (required for upload) - with open(os.path.join(tmp_dir, "requirements.txt"), "w") as f: - f.write("eval-protocol>=0.1.0\n") - return tmp_dir - - -def create_sample_file(): - fd, path = tempfile.mkstemp(suffix=".jsonl") - samples = [ - { - "messages": [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi there! How can I help you today?"}, - ] - }, - { - "messages": [ - {"role": "user", "content": "What is AI?"}, - { - "role": "assistant", - "content": "AI stands for Artificial Intelligence.", - }, - ], - "tools": [ - { - "type": "function", - "function": { - "name": "search", - "description": "Search for information", - }, - } - ], - }, - ] - with os.fdopen(fd, "w") as f: - for sample in samples: - f.write(json.dumps(sample) + "\n") - return path - - -@pytest.fixture -def mock_env_variables(monkeypatch): - monkeypatch.setenv("FIREWORKS_API_KEY", "test_api_key") - monkeypatch.setenv("FIREWORKS_API_BASE", "https://api.fireworks.ai") - monkeypatch.setattr("eval_protocol.evaluation.get_fireworks_account_id", lambda: "test_account") - - -@pytest.fixture -def mock_requests_get(): - """Mock requests.get for force flow check""" - with patch("requests.get") as mock_get: - mock_get.return_value.status_code = 404 # Evaluator doesn't exist - mock_get.return_value.raise_for_status = MagicMock() - yield mock_get - - -@pytest.fixture -def mock_requests_delete(): - """Mock requests.delete for force flow""" - with patch("requests.delete") as mock_delete: - mock_delete.return_value.status_code = 200 - mock_delete.return_value.raise_for_status = MagicMock() - yield mock_delete - - -@pytest.fixture -def mock_gcs_upload(): - """Mock the GCS upload via requests.Session""" - with patch("requests.Session") as mock_session_class: - mock_session = MagicMock() - mock_session_class.return_value = mock_session - - # Mock successful GCS upload - mock_gcs_response = MagicMock() - mock_gcs_response.status_code = 200 - mock_gcs_response.raise_for_status = MagicMock() - mock_session.send.return_value = mock_gcs_response - - yield mock_session - - -@pytest.fixture -def mock_requests_post(): - with patch("requests.post") as mock_post: - default_response = { - "name": "accounts/test_account/evaluators/test-eval", - "displayName": "Test Evaluator", - "description": "Test description", - "multiMetrics": False, - } - preview_response = { - "totalSamples": 2, - "totalRuntimeMs": 1234, - "results": [ - { - "success": True, - "score": 0.7, - "perMetricEvals": {"quality": 0.8, "relevance": 0.7, "safety": 0.9}, - }, - { - "success": True, - "score": 0.5, - "perMetricEvals": {"quality": 0.6, "relevance": 0.4, "safety": 0.8}, - }, - ], - } - validate_response = {"success": True, "valid": True} - - def side_effect(*args, **kwargs): - url = args[0] - payload = kwargs.get("json", {}) - response = mock_post.return_value - if "previewEvaluator" in url: - response.json.return_value = preview_response - elif "getUploadEndpoint" in url: - # Dynamically create signed URLs for whatever filenames are requested - filename_to_size = payload.get("filename_to_size", {}) - signed_urls = {} - for filename in filename_to_size.keys(): - signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" - response.json.return_value = {"filenameToSignedUrls": signed_urls} - elif "validateUpload" in url: - response.json.return_value = validate_response - else: - response.json.return_value = default_response - return response - - mock_post.side_effect = side_effect - mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = default_response - mock_post.return_value.raise_for_status = MagicMock() - yield mock_post - - -def test_integration_single_metric(mock_env_variables, mock_requests_post, mock_gcs_upload): - tmp_dir = create_test_folder() - sample_file = create_sample_file() - original_cwd = os.getcwd() - try: - os.chdir(tmp_dir) - preview_result = preview_evaluation( - metric_folders=[f"test_metric={tmp_dir}"], - sample_file=sample_file, - max_samples=2, - ) - assert preview_result.total_samples == 2 - assert len(preview_result.results) == 2 - evaluator = create_evaluation( - evaluator_id="test-eval", - metric_folders=[f"test_metric={tmp_dir}"], - display_name="Test Evaluator", - description="Test description", - ) - assert evaluator["name"] == "accounts/test_account/evaluators/test-eval" - assert evaluator["displayName"] == "Test Evaluator" - - # Verify all API calls in the new upload flow - post_calls = [call[0][0] for call in mock_requests_post.call_args_list] - - # 1. Create evaluator call (V2 endpoint) - assert any("evaluatorsV2" in url for url in post_calls), "Should call V2 create endpoint" - - # 2. Get upload endpoint call - assert any("getUploadEndpoint" in url for url in post_calls), "Should call getUploadEndpoint" - - # 3. Validate upload call - assert any("validateUpload" in url for url in post_calls), "Should call validateUpload" - - # 4. Verify GCS upload happened - assert mock_gcs_upload.send.called, "Should upload tar.gz to GCS" - gcs_request = mock_gcs_upload.send.call_args[0][0] - assert gcs_request.method == "PUT", "GCS upload should use PUT" - assert "storage.googleapis.com" in gcs_request.url, "Should upload to GCS" - - # Verify create payload structure - create_call_payload = None - for call in mock_requests_post.call_args_list: - url = call[0][0] - if "evaluatorsV2" in url: - create_call_payload = call[1].get("json") - break - - assert create_call_payload is not None, "Should have create payload" - assert "evaluator" in create_call_payload - assert "evaluatorId" in create_call_payload and create_call_payload["evaluatorId"] == "test-eval" - assert "criteria" in create_call_payload["evaluator"] - assert len(create_call_payload["evaluator"]["criteria"]) > 0 - assert create_call_payload["evaluator"]["criteria"][0]["type"] == "CODE_SNIPPETS" - finally: - os.chdir(original_cwd) - shutil.rmtree(tmp_dir, ignore_errors=True) - os.unlink(sample_file) - - -def test_integration_multi_metrics(mock_env_variables, mock_requests_post, mock_gcs_upload): - tmp_dir = create_test_folder() - sample_file = create_sample_file() - original_cwd = os.getcwd() - try: - os.chdir(tmp_dir) - preview_result = preview_evaluation(multi_metrics=True, folder=tmp_dir, sample_file=sample_file, max_samples=2) - assert preview_result.total_samples == 2 - assert len(preview_result.results) == 2 - assert hasattr(preview_result.results[0], "per_metric_evals") - assert "quality" in preview_result.results[0].per_metric_evals - mock_requests_post.reset_mock() - mock_requests_post.return_value.json.return_value = { - "name": "accounts/test_account/evaluators/test-eval", - "displayName": "Multi Metrics Evaluator", - "description": "Test multi-metrics evaluator", - "multiMetrics": True, - } - evaluator = create_evaluation( - evaluator_id="multi-metrics-eval", - multi_metrics=True, - folder=tmp_dir, - display_name="Multi Metrics Evaluator", - description="Test multi-metrics evaluator", - ) - assert evaluator["name"] == "accounts/test_account/evaluators/test-eval" - - # Verify all API calls in the new upload flow - post_calls = [call[0][0] for call in mock_requests_post.call_args_list] - assert any("evaluatorsV2" in url for url in post_calls), "Should call V2 create endpoint" - assert any("getUploadEndpoint" in url for url in post_calls), "Should call getUploadEndpoint" - assert any("validateUpload" in url for url in post_calls), "Should call validateUpload" - - # Verify GCS upload happened - assert mock_gcs_upload.send.called, "Should upload tar.gz to GCS" - - # Verify create payload uses V2 format - create_call_payload = None - for call in mock_requests_post.call_args_list: - url = call[0][0] - if "evaluatorsV2" in url: - create_call_payload = call[1].get("json") - break - - assert create_call_payload is not None - assert "evaluator" in create_call_payload - assert create_call_payload["evaluatorId"] == "multi-metrics-eval" - assert create_call_payload["evaluator"]["multiMetrics"] is True - finally: - import shutil - - os.chdir(original_cwd) - shutil.rmtree(tmp_dir, ignore_errors=True) - os.unlink(sample_file) - - -@patch("sys.exit") -def test_integration_cli_commands(mock_sys_exit, mock_env_variables, mock_requests_post): # Corrected parameter name - from eval_protocol.cli import deploy_command, preview_command - - mock_sys_exit.side_effect = lambda code=0: None - - tmp_dir = create_test_folder() - sample_file = create_sample_file() - original_cwd = os.getcwd() - try: - os.chdir(tmp_dir) - # Test preview command - with patch("eval_protocol.cli_commands.preview.preview_evaluation") as mock_preview_eval_func: - mock_preview_result = MagicMock() - mock_preview_result.display = MagicMock() - mock_preview_eval_func.return_value = mock_preview_result - args = MagicMock() - args.metrics_folders = [f"test_metric={tmp_dir}"] - args.samples = sample_file - args.max_samples = 2 - args.huggingface_dataset = None - args.huggingface_split = "train" - args.huggingface_prompt_key = "prompt" - args.huggingface_response_key = "response" - args.huggingface_key_map = None - args.remote_url = None # Explicitly set for local path - - with patch("eval_protocol.cli_commands.preview.Path.exists", return_value=True): - result = preview_command(args) - assert result == 0 - mock_preview_eval_func.assert_called_once_with( - metric_folders=[f"test_metric={tmp_dir}"], - sample_file=sample_file, - max_samples=2, - huggingface_dataset=None, - huggingface_split="train", - huggingface_prompt_key="prompt", - huggingface_response_key="response", - huggingface_message_key_map=None, - ) - mock_preview_result.display.assert_called_once() - - # Test deploy command - with patch("eval_protocol.cli_commands.deploy.create_evaluation") as mock_create_eval_func: - mock_create_eval_func.return_value = { - "name": "accounts/test_account/evaluators/test-eval", - "displayName": "Test Evaluator", - "description": "Test description", - "multiMetrics": False, - } - args = MagicMock() - args.metrics_folders = [f"test_metric={tmp_dir}"] - args.id = "test-eval" - args.display_name = "Test Evaluator" - args.description = "Test description" - args.force = False - args.huggingface_dataset = None - args.huggingface_split = "train" - args.huggingface_prompt_key = "prompt" - args.huggingface_response_key = "response" - args.huggingface_key_map = None - args.remote_url = None # Explicitly set for local path - args.target = "fireworks" # Explicitly set target for this test path - - result = deploy_command(args) - assert result == 0 - mock_create_eval_func.assert_called_once_with( - evaluator_id="test-eval", - metric_folders=[f"test_metric={tmp_dir}"], - display_name="Test Evaluator", - description="Test description", - force=False, - huggingface_dataset=None, - huggingface_split="train", - huggingface_message_key_map=None, - huggingface_prompt_key="prompt", - huggingface_response_key="response", - ) - finally: - os.chdir(original_cwd) - import shutil - - shutil.rmtree(tmp_dir, ignore_errors=True) - os.unlink(sample_file) diff --git a/tests/test_evaluation_preview_integration.py b/tests/test_evaluation_preview_integration.py deleted file mode 100644 index d7b3b266..00000000 --- a/tests/test_evaluation_preview_integration.py +++ /dev/null @@ -1,470 +0,0 @@ -import importlib.util -import json -import os -import sys -import tempfile -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest - - -# Load the evaluation_preview_example module directly from the examples folder -def load_module_from_path(name, path): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None: - raise ImportError(f"Could not load spec for module {name} from {path}") - module = importlib.util.module_from_spec(spec) - if spec.loader is None: - raise ImportError(f"Spec for module {name} has no loader") - spec.loader.exec_module(module) - return module - - -@pytest.fixture -def evaluation_preview_example(): - # Path to the evaluation_preview_example.py file - file_path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - "examples", - "evaluation_preview_example.py", - ) - - # Load the module - return load_module_from_path("evaluation_preview_example", file_path) - - -@pytest.fixture -def mock_env_variables(monkeypatch): - """Set environment variables for testing""" - monkeypatch.setenv("FIREWORKS_API_KEY", "test_api_key") - monkeypatch.setenv("FIREWORKS_API_BASE", "https://api.fireworks.ai") - monkeypatch.setattr("eval_protocol.evaluation.get_fireworks_account_id", lambda: "test_account") - - -@pytest.fixture -def mock_preview_api(): - """Mock the preview API calls""" - with patch("requests.post") as mock_post: - # Set up mock for preview API - preview_response = { - "totalSamples": 2, - "totalRuntimeMs": 1234, - "results": [ - { - "success": True, - "score": 0.26, - "perMetricEvals": { - "word_count": { - "score": 0.26, - "reason": "Word count: 26", - } - }, - }, - { - "success": True, - "score": 0.22, - "perMetricEvals": { - "word_count": { - "score": 0.22, - "reason": "Word count: 22", - } - }, - }, - ], - } - - mock_post.return_value = MagicMock() - mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = preview_response - - yield mock_post - - -@pytest.fixture -def mock_create_api(): - """Mock the create API calls""" - with patch("requests.post") as mock_post: - create_response = { - "name": "accounts/test_account/evaluators/word-count-eval", - "displayName": "Word Count Evaluator", - "description": "Evaluates responses based on word count", - } - - def side_effect(*args, **kwargs): - url = args[0] - payload = kwargs.get("json", {}) - response = mock_post.return_value - - if "getUploadEndpoint" in url: - # Return signed URL for upload - filename_to_size = payload.get("filename_to_size", {}) - signed_urls = {} - for filename in filename_to_size.keys(): - signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" - response.json.return_value = {"filenameToSignedUrls": signed_urls} - elif "validateUpload" in url: - response.json.return_value = {"success": True, "valid": True} - else: - response.json.return_value = create_response - - response.status_code = 200 - return response - - mock_post.side_effect = side_effect - mock_post.return_value = MagicMock() - mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = create_response - mock_post.return_value.raise_for_status = MagicMock() - - yield mock_post - - -@pytest.fixture -def mock_gcs_upload(): - """Mock the GCS upload via requests.Session""" - with patch("requests.Session") as mock_session_class: - mock_session = MagicMock() - mock_session_class.return_value = mock_session - - # Mock successful GCS upload - mock_gcs_response = MagicMock() - mock_gcs_response.status_code = 200 - mock_gcs_response.raise_for_status = MagicMock() - mock_session.send.return_value = mock_gcs_response - - yield mock_session - - -@pytest.fixture -def mock_word_count_metric(): - """Create a temporary directory with a word count metric""" - tmp_dir = tempfile.mkdtemp() - - # Create the metrics/word_count directory - os.makedirs(os.path.join(tmp_dir, "metrics", "word_count"), exist_ok=True) - - # Create main.py in the word_count directory - with open(os.path.join(tmp_dir, "metrics", "word_count", "main.py"), "w") as f: - f.write( - """ -def evaluate(messages, ground_truth=None, tools=None, **kwargs): - if not messages: - return {'score': 0.0, 'reason': 'No messages found'} - - last_message = messages[-1] - content = last_message.get('content', '') - - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - - return { - 'score': score, - 'reason': f'Word count: {word_count}' - } -""" - ) - - # Create a samples directory and sample file - os.makedirs(os.path.join(tmp_dir, "samples"), exist_ok=True) - - # Create a sample file - with open(os.path.join(tmp_dir, "samples", "samples.jsonl"), "w") as f: - f.write( - json.dumps( - { - "messages": [ - {"role": "user", "content": "Hello"}, - { - "role": "assistant", - "content": "Hi there! How can I help you today?", - }, - ] - } - ) - + "\n" - ) - f.write( - json.dumps( - { - "messages": [ - {"role": "user", "content": "What is AI?"}, - { - "role": "assistant", - "content": "AI stands for Artificial Intelligence.", - }, - ] - } - ) - + "\n" - ) - - yield tmp_dir - - # Clean up - import shutil - - shutil.rmtree(tmp_dir) - - -def test_preview_evaluation(mock_env_variables, mock_preview_api, monkeypatch): - """Test the preview_evaluation function in isolation""" - from eval_protocol.evaluation import preview_evaluation - - # Create a temporary directory for the test - with tempfile.TemporaryDirectory() as tmp_dir: - # Create a metrics directory with word_count - os.makedirs(os.path.join(tmp_dir, "word_count"), exist_ok=True) - - # Create main.py in the word_count directory - with open(os.path.join(tmp_dir, "word_count", "main.py"), "w") as f: - f.write( - """ -def evaluate(messages, ground_truth=None, tools=None, **kwargs): - if not messages: - return {'score': 0.0, 'reason': 'No messages found'} - - last_message = messages[-1] - content = last_message.get('content', '') - - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - - return { - 'score': score, - 'reason': f'Word count: {word_count}' - } -""" - ) - - # Create a temporary sample file - sample_fd, sample_path = tempfile.mkstemp(suffix=".jsonl") - with os.fdopen(sample_fd, "w") as f: - f.write( - json.dumps( - { - "messages": [ - {"role": "user", "content": "Hello"}, - { - "role": "assistant", - "content": "Hi there! How can I help you today?", - }, - ] - } - ) - + "\n" - ) - f.write( - json.dumps( - { - "messages": [ - {"role": "user", "content": "What is AI?"}, - { - "role": "assistant", - "content": "AI stands for Artificial Intelligence.", - }, - ] - } - ) - + "\n" - ) - - # Set used_preview_api flag to simulate successful preview - import eval_protocol.evaluation - - eval_protocol.evaluation.used_preview_api = True - - # Call preview_evaluation - result = preview_evaluation( - metric_folders=[f"word_count={os.path.join(tmp_dir, 'word_count')}"], - sample_file=sample_path, - max_samples=2, - ) - - # Clean up - os.unlink(sample_path) - - # Verify results - assert result.total_samples == 2 - assert len(result.results) == 2 - # Assuming result.results[0] is an object, use attribute access - assert result.results[0].score == 0.26 - assert hasattr(result.results[0], "per_metric_evals") - assert "word_count" in result.results[0].per_metric_evals - - -def test_create_evaluation(mock_env_variables, mock_create_api, mock_gcs_upload, monkeypatch): - """Test the create_evaluation function in isolation""" - from eval_protocol.evaluation import create_evaluation - - # Create a temporary directory for the test - with tempfile.TemporaryDirectory() as tmp_dir: - # Create a metrics directory with word_count - os.makedirs(os.path.join(tmp_dir, "word_count"), exist_ok=True) - - # Create main.py in the word_count directory - with open(os.path.join(tmp_dir, "word_count", "main.py"), "w") as f: - f.write( - """ -def evaluate(messages, ground_truth=None, tools=None, **kwargs): - if not messages: - return {'score': 0.0, 'reason': 'No messages found'} - - last_message = messages[-1] - content = last_message.get('content', '') - - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - - return { - 'score': score, - 'reason': f'Word count: {word_count}' - } -""" - ) - - # Create requirements.txt - with open(os.path.join(tmp_dir, "requirements.txt"), "w") as f: - f.write("eval-protocol>=0.1.0\n") - - # Change to temp directory - original_cwd = os.getcwd() - os.chdir(tmp_dir) - - try: - # Call create_evaluation - result = create_evaluation( - evaluator_id="word-count-eval", - metric_folders=[f"word_count={os.path.join(tmp_dir, 'word_count')}"], - display_name="Word Count Evaluator", - description="Evaluates responses based on word count", - force=True, - ) - - # Verify results - assert result["name"] == "accounts/test_account/evaluators/word-count-eval" - assert result["displayName"] == "Word Count Evaluator" - assert result["description"] == "Evaluates responses based on word count" - finally: - os.chdir(original_cwd) - - -def test_preview_then_create(monkeypatch, mock_env_variables, mock_preview_api, mock_create_api, mock_gcs_upload): - """Test the full example flow (simulated)""" - # Patch input to always return 'y' - monkeypatch.setattr("builtins.input", lambda _: "y") - - with tempfile.TemporaryDirectory() as tmp_dir: - # Create a metrics directory with word_count - os.makedirs(os.path.join(tmp_dir, "word_count"), exist_ok=True) - - # Create main.py in the word_count directory - with open(os.path.join(tmp_dir, "word_count", "main.py"), "w") as f: - f.write( - """ -def evaluate(messages, ground_truth=None, tools=None, **kwargs): - if not messages: - return {'score': 0.0, 'reason': 'No messages found'} - - last_message = messages[-1] - content = last_message.get('content', '') - - word_count = len(content.split()) - score = min(word_count / 100, 1.0) - - return { - 'score': score, - 'reason': f'Word count: {word_count}' - } -""" - ) - - # Create requirements.txt - with open(os.path.join(tmp_dir, "requirements.txt"), "w") as f: - f.write("eval-protocol>=0.1.0\n") - - # Create a temporary sample file - sample_fd, sample_path = tempfile.mkstemp(suffix=".jsonl") - with os.fdopen(sample_fd, "w") as f: - f.write( - json.dumps( - { - "messages": [ - {"role": "user", "content": "Hello"}, - { - "role": "assistant", - "content": "Hi there! How can I help you today?", - }, - ] - } - ) - + "\n" - ) - f.write( - json.dumps( - { - "messages": [ - {"role": "user", "content": "What is AI?"}, - { - "role": "assistant", - "content": "AI stands for Artificial Intelligence.", - }, - ] - } - ) - + "\n" - ) - - # Create a patched example module with modified paths - from eval_protocol.evaluation import create_evaluation, preview_evaluation - - # Change to temp directory - original_cwd = os.getcwd() - os.chdir(tmp_dir) - - try: - # Define a patched main function - def patched_main(): - # Preview the evaluation using metrics folder and samples file - print("Previewing evaluation...") - preview_result = preview_evaluation( - metric_folders=[f"word_count={os.path.join(tmp_dir, 'word_count')}"], - sample_file=sample_path, - max_samples=2, - ) - - preview_result.display() - - # Check if 'used_preview_api' attribute exists and is True - import eval_protocol.evaluation as evaluation_module - - # For testing, always assume the API was used successfully - evaluation_module.used_preview_api = True - - print("\nCreating evaluation...") - try: - evaluator = create_evaluation( - evaluator_id="word-count-eval", - metric_folders=[f"word_count={os.path.join(tmp_dir, 'word_count')}"], - display_name="Word Count Evaluator", - description="Evaluates responses based on word count", - force=True, - ) - print(f"Created evaluator: {evaluator['name']}") - return evaluator - except Exception as e: - print(f"Error creating evaluator: {str(e)}") - print("Make sure you have proper Fireworks API credentials set up.") - return None - - # Run the patched main function - result = patched_main() - - # Clean up - os.unlink(sample_path) - - # Verify the result - assert result is not None - assert result["name"] == "accounts/test_account/evaluators/word-count-eval" - finally: - os.chdir(original_cwd) From 7d1a67b058d3deeb02b8d55087e6f3c27a1c03a4 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Wed, 24 Dec 2025 11:00:07 -0800 Subject: [PATCH 13/16] delete more legacy code --- eval_protocol/cli_commands/upload.py | 3 - eval_protocol/evaluation.py | 349 +-------------------------- tests/test_evaluation.py | 72 +----- 3 files changed, 14 insertions(+), 410 deletions(-) diff --git a/eval_protocol/cli_commands/upload.py b/eval_protocol/cli_commands/upload.py index ceb38a86..9e079c3e 100644 --- a/eval_protocol/cli_commands/upload.py +++ b/eval_protocol/cli_commands/upload.py @@ -268,11 +268,8 @@ def upload_command(args: argparse.Namespace) -> int: print(f"\nUploading evaluator '{evaluator_id}' for {qualname.split('.')[-1]}...") try: - test_dir = root - metric_name = os.path.basename(test_dir) or "metric" result = create_evaluation( evaluator_id=evaluator_id, - metric_folders=[f"{metric_name}={test_dir}"], display_name=display_name or evaluator_id, description=description or f"Evaluator for {qualname}", force=force, diff --git a/eval_protocol/evaluation.py b/eval_protocol/evaluation.py index ab91e9a3..9c84d34e 100644 --- a/eval_protocol/evaluation.py +++ b/eval_protocol/evaluation.py @@ -1,312 +1,36 @@ -import ast # Added for AST parsing -import importlib.util # Added for dynamic module loading -import json import logging import os -import sys # Added for path manipulation import time -import types -from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast +from typing import List, Optional -if TYPE_CHECKING: - # For type checking only - import datasets - -from fireworks import Fireworks import fireworks import requests +from fireworks import Fireworks from eval_protocol.auth import ( get_fireworks_account_id, get_fireworks_api_key, verify_api_key_and_get_account_id, ) -from eval_protocol.typed_interface import EvaluationMode - from eval_protocol.get_pep440_version import get_pep440_version logger = logging.getLogger(__name__) -def huggingface_dataset_to_jsonl( - dataset_name: str, - split: str = "train", - output_file: Optional[str] = None, - max_samples: int = 100, - message_key_map: Optional[Dict[str, str]] = None, - response_key: str = "response", - prompt_key: str = "prompt", -) -> str: - """ - Converts a HuggingFace dataset to JSONL format suitable for Eval Protocol evaluation. - - Args: - dataset_name: The name of the HuggingFace dataset (e.g., "deepseek-ai/DeepSeek-ProverBench") - split: The dataset split to use (default: "train") - output_file: Optional file path to save the JSONL output (if None, generates a temp file) - max_samples: Maximum number of samples to include - message_key_map: Optional mapping of dataset keys to Eval Protocol message keys - response_key: Key in the dataset containing the response text (default: "response") - prompt_key: Key in the dataset containing the prompt text (default: "prompt") - - Returns: - Path to the generated JSONL file - """ - try: - from datasets import load_dataset # pyright: ignore[reportAttributeAccessIssue] - except ImportError: - raise ImportError( - "The 'datasets' package is required to use this function. " - "Please install it with 'pip install \"eval-protocol[deepseek]\"'" - ) - - import tempfile - - logger.info(f"Loading dataset {dataset_name} (split: {split})") - dataset = load_dataset(dataset_name, split=split) - - if not output_file: - temp_dir = tempfile.gettempdir() - dataset_basename = dataset_name.split("/")[-1] - output_file = os.path.join(temp_dir, f"{dataset_basename}_{split}_{int(time.time())}.jsonl") - - os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True) - - if message_key_map is None: - message_key_map = {} - - processed_samples = 0 - # Initialize i to handle empty dataset case for logging - i = -1 - with open(output_file, "w") as f: - for i, item in enumerate(dataset): - if processed_samples >= max_samples: - break - - if prompt_key not in item and "statement" not in item: - logger.debug(f"Skipping sample {i} due to missing prompt/statement key.") - continue - - prompt_text = item.get(prompt_key, item.get("statement", "")) - response_text = item.get( - response_key, - item.get("reference_solution", item.get("expected_proof", "")), - ) - - if not prompt_text or not response_text: - logger.debug(f"Skipping sample {i} due to missing prompt or response text.") - continue - - messages = [ - {"role": "user", "content": prompt_text}, - {"role": "assistant", "content": response_text}, - ] - entry = {"messages": messages} - - for ds_key, rk_key in message_key_map.items(): - if ds_key in item: - entry[rk_key] = item[ds_key] - - for key, value in item.items(): - if key not in [prompt_key, response_key] and key not in message_key_map: - entry[key] = value - - f.write(json.dumps(entry) + "\n") - processed_samples += 1 - - if processed_samples == 0 and i == -1: - logger.info(f"No samples converted to JSONL format: {output_file}") - else: - logger.info(f"Converted {processed_samples} samples to JSONL format: {output_file}") - return output_file - - class Evaluator: def __init__( self, - multi_metrics=False, # Relates to output structure (dict of metrics vs single) - remote_url: Optional[str] = None, - ts_mode_config: Optional[Dict[str, Any]] = None, - reward_function_mode: EvaluationMode = "pointwise", # New parameter for input processing mode account_id: Optional[str] = None, api_key: Optional[str] = None, entry_point: Optional[str] = None, ): - self.multi_metrics = multi_metrics - self.remote_url = remote_url - self.ts_mode_config = ts_mode_config - self.reward_function_mode = reward_function_mode - self.code_files = {} - self.metric_folders: Dict[str, Dict[str, Any]] = {} # Changed to store path and requirements self.account_id = account_id self.api_key = api_key self.description = "" self.display_name = "" self.api_base = os.environ.get("FIREWORKS_API_BASE", "https://api.fireworks.ai") - # Optional requirements string for multi-metric mode (when loaded differently) - self._loaded_multi_metric_requirements_str: Optional[str] = None - # Optional entry point metadata (module::function or path::function) self.entry_point: Optional[str] = entry_point - if self.ts_mode_config: - python_code = self.ts_mode_config.get("python_code") - file_name = self.ts_mode_config.get("file_name", "main.py") - if not python_code: - raise ValueError("python_code is required in ts_mode_config") - self.code_files[file_name] = python_code - - def _should_include_file(self, filename: str) -> bool: - """Check if a file should be included in the evaluator upload.""" - return ( - filename.endswith(".py") - or filename.endswith(".txt") - or filename.endswith(".toml") - or os.path.basename(filename) == "Dockerfile" - ) - - def _load_python_files_from_folder(self, folder_path: str) -> Dict[str, str]: - """ - Recursively loads Python, text, and TOML files from a given folder (excluding common ignored dirs). - - Args: - folder_path: Absolute path to the folder. - - Returns: - A dictionary mapping relative file paths (within folder) to their content. - - Raises: - ValueError: If folder_path is invalid or not a directory. - """ - if not os.path.exists(folder_path): - raise ValueError(f"Folder does not exist: {folder_path}") - - if not os.path.isdir(folder_path): - raise ValueError(f"Not a directory: {folder_path}") - - files: Dict[str, str] = {} - ignored_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "dist", "build", "vendor"} - base_path = Path(folder_path) - for dirpath, dirnames, filenames in os.walk(folder_path): - # prune ignored directories - dirnames[:] = [d for d in dirnames if d not in ignored_dirs and not d.startswith(".")] - for name in filenames: - if not self._should_include_file(name): - continue - abs_path = Path(dirpath) / name - rel_path = str(abs_path.relative_to(base_path)) - with open(abs_path, "r", encoding="utf-8") as f: - content = f.read() - files[rel_path] = content - if not files: - raise ValueError(f"No Python, text, or TOML files found in {folder_path}") - return files - - def load_metric_folder(self, metric_name, folder_path): - """ - Load code files from a metric folder - - Args: - metric_name: Name of the metric - folder_path: Path to the folder containing code files - - Returns: - Dict mapping filenames to their contents - """ - folder_path = os.path.abspath(folder_path) - files = self._load_python_files_from_folder(folder_path) # Reads all .py files into a dict - metric_requirements_list: Optional[List[str]] = None - - main_py_content = files.get("main.py") - if main_py_content: - try: - tree = ast.parse(main_py_content) - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef) and node.name == "evaluate": - for decorator_node in node.decorator_list: - if ( - isinstance(decorator_node, ast.Call) - and isinstance(decorator_node.func, ast.Name) - and decorator_node.func.id == "reward_function" - ): - for keyword in decorator_node.keywords: - if keyword.arg == "requirements": - if isinstance(keyword.value, ast.List): - reqs: List[str] = [] - for elt in keyword.value.elts: - if isinstance(elt, ast.Constant): # Python 3.8+ - if isinstance(elt.value, str): - reqs.append(cast(str, elt.value)) - elif isinstance(elt, ast.Str): # Python < 3.8 - reqs.append(cast(str, elt.s)) - if reqs: - metric_requirements_list = cast(List[str], reqs) - elif isinstance(keyword.value, ast.Constant) and isinstance( - keyword.value.value, str - ): # Python 3.8+ (single req string) - metric_requirements_list = [cast(str, keyword.value.value)] - elif isinstance(keyword.value, ast.Str): # Python < 3.8 (single req string) - metric_requirements_list = [cast(str, keyword.value.s)] - break - if metric_requirements_list: - break - if metric_requirements_list: - logger.info( - f"Found requirements for metric '{metric_name}' via AST: {metric_requirements_list}" - ) - break - except SyntaxError as e: - logger.error(f"Syntax error parsing main.py for metric '{metric_name}' to find requirements: {e}") - except Exception as e: - logger.error(f"Error parsing main.py AST for metric '{metric_name}': {e}") - - self.metric_folders[metric_name] = { - "path": folder_path, - "requirements": metric_requirements_list, # This is now a list of strings or None - } - - for filename, content in files.items(): - self.code_files[f"{metric_name}/{filename}"] = content - - logger.info(f"Loaded {len(files)} files for metric '{metric_name}' from {folder_path}") - return files - - def load_multi_metrics_folder(self, folder_path): - """ - Load code files from a folder with multiple metrics - - Args: - folder_path: Path to the folder containing code files - - Returns: - Dict mapping filenames to their contents - """ - folder_path = os.path.abspath(folder_path) - files = self._load_python_files_from_folder(folder_path) - - self.code_files = files - logger.info(f"Loaded {len(files)} files from {folder_path} for multi-metrics evaluation") - return files - - def load_samples_from_jsonl(self, sample_file, max_samples=5): - if not os.path.exists(sample_file): - raise ValueError(f"Sample file does not exist: {sample_file}") - samples = [] - with open(sample_file, "r") as f: - for i, line in enumerate(f): - if i >= max_samples: - break - line = line.strip() - if not line: - continue - try: - sample = json.loads(line) - samples.append(sample) - except json.JSONDecodeError: - logger.warning(f"Invalid JSON on line {i + 1}, skipping") - logger.info(f"Loaded {len(samples)} samples from {sample_file}") - return samples - @staticmethod def _parse_ignore_file(ignore_path: str) -> List[str]: """Parse .gitignore or .dockerignore and return patterns.""" @@ -431,9 +155,6 @@ def _create_tar_gz_with_ignores(output_path: str, source_dir: str) -> int: return size_bytes def create(self, evaluator_id, display_name=None, description=None, force=False): - if not self.remote_url and not self.ts_mode_config and not self.code_files: - raise ValueError("No code files loaded. Load metric folder(s) or provide ts_mode_config/remote_url first.") - auth_token = self.api_key or get_fireworks_api_key() account_id = self.account_id or get_fireworks_account_id() if not account_id and auth_token: @@ -639,73 +360,29 @@ def _get_authentication(self): # Helper functions for CLI commands def create_evaluation( evaluator_id: str, - metric_folders: Optional[List[str]] = None, - multi_metrics: bool = False, # Original folder-based multi_metrics flag - folder: Optional[str] = None, - python_code_to_evaluate: Optional[str] = None, - python_file_name_for_code: str = "main.py", - criterion_name_for_code: str = "default_code_criterion", - criterion_description_for_code: str = "Python code execution", display_name: Optional[str] = None, description: Optional[str] = None, force: bool = False, - huggingface_dataset: Optional[str] = None, - huggingface_split: str = "train", - huggingface_message_key_map: Optional[Dict[str, str]] = None, - huggingface_response_key: str = "response", - huggingface_prompt_key: str = "prompt", - remote_url: Optional[str] = None, - reward_function_mode: EvaluationMode = "pointwise", # Added account_id: Optional[str] = None, api_key: Optional[str] = None, entry_point: Optional[str] = None, ): - ts_mode_config = None - if python_code_to_evaluate: - if metric_folders or folder: # Removed multi_metrics from this check - raise ValueError("Cannot use python_code_to_evaluate with folder-based parameters.") - ts_mode_config = { - "python_code": python_code_to_evaluate, - "file_name": python_file_name_for_code, - "criterion_name": criterion_name_for_code, - "description": criterion_description_for_code, - } + """ + Create an evaluator on the Fireworks platform. + Args: + evaluator_id: Unique identifier for the evaluator + display_name: Display name for the evaluator + description: Description for the evaluator + force: If True, delete and recreate if evaluator exists + account_id: Optional Fireworks account ID + api_key: Optional Fireworks API key + entry_point: Optional entry point (module::function or path::function) + """ evaluator = Evaluator( - multi_metrics=multi_metrics, - remote_url=remote_url, - ts_mode_config=ts_mode_config, - reward_function_mode=reward_function_mode, account_id=account_id, api_key=api_key, entry_point=entry_point, ) - if remote_url: - logger.info(f"Configuring evaluator to use remote URL: {remote_url}") - if ( - metric_folders or folder or python_code_to_evaluate - ): # If remote_url, other code sources are ignored for execution - logger.warning( - "When remote_url is provided, other code sources (folders, python_code_to_evaluate) are ignored for execution logic by the platform." - ) - elif ts_mode_config: - # ts_mode_config already handled in Evaluator.__init__ for self.code_files - logger.info("Configuring evaluator with direct Python code snippet (ts_mode).") - elif multi_metrics: # Folder-based multi_metrics - if not folder: - raise ValueError("`folder` must be specified for folder-based multi_metrics mode.") - evaluator.load_multi_metrics_folder(folder) - else: # Folder-based single/multiple metrics (non-multi_metrics structure) - if not metric_folders: - raise ValueError("At least one metric_folder must be specified.") - for pair in metric_folders: - if "=" not in pair: - raise ValueError(f"Invalid metric-folder format: {pair}.") - metric_name, folder_path = pair.split("=", 1) - evaluator.load_metric_folder(metric_name, folder_path) - - if huggingface_dataset: - logger.info(f"HuggingFace dataset specified: {huggingface_dataset}") - return evaluator.create(evaluator_id, display_name, description, force) diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index c95156a7..942c1962 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -1,12 +1,9 @@ -import json import os import shutil import tempfile from unittest.mock import MagicMock, patch -import requests - -from eval_protocol.evaluation import Evaluator, create_evaluation +from eval_protocol.evaluation import create_evaluation def create_test_folder(): @@ -34,72 +31,6 @@ def evaluate(messages, original_messages=None, tools=None, **kwargs): return tmp_dir -def create_sample_file(): - fd, path = tempfile.mkstemp(suffix=".jsonl") - samples = [ - { - "messages": [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi there! How can I help you today?"}, - ] - }, - { - "messages": [ - {"role": "user", "content": "What is AI?"}, - { - "role": "assistant", - "content": "AI stands for Artificial Intelligence.", - }, - ], - "original_messages": [ - {"role": "user", "content": "What is AI?"}, - { - "role": "assistant", - "content": "AI stands for Artificial Intelligence.", - }, - ], - "tools": [ - { - "type": "function", - "function": { - "name": "search", - "description": "Search for information", - }, - } - ], - }, - ] - with os.fdopen(fd, "w") as f: - for sample in samples: - f.write(json.dumps(sample) + "\n") - return path - - -def test_evaluator_load_metric_folder(): - tmp_dir = create_test_folder() - try: - evaluator = Evaluator() - files = evaluator.load_metric_folder("test_metric", tmp_dir) - assert "main.py" in files - assert "test_metric" in evaluator.metric_folders - assert "test_metric/main.py" in evaluator.code_files - assert "evaluate" in evaluator.code_files["test_metric/main.py"] - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) - - -def test_evaluator_load_multi_metrics_folder(): - tmp_dir = create_test_folder() - try: - evaluator = Evaluator(multi_metrics=True) - files = evaluator.load_multi_metrics_folder(tmp_dir) - assert "main.py" in files - assert "main.py" in evaluator.code_files - assert "evaluate" in evaluator.code_files["main.py"] - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) - - def test_create_evaluation_helper(monkeypatch): tmp_dir = create_test_folder() monkeypatch.setenv("FIREWORKS_API_KEY", "test_api_key") @@ -167,7 +98,6 @@ def mock_validate_upload(evaluator_id, body): os.chdir(tmp_dir) api_response = create_evaluation( evaluator_id="test-eval", - metric_folders=[f"test_metric={tmp_dir}"], display_name="Test Evaluator", description="Test description", ) From 1d7d807f8e64433ec8005712cb46273affd9f19e Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Fri, 26 Dec 2025 14:08:12 -0800 Subject: [PATCH 14/16] fix test --- tests/test_ep_upload_e2e.py | 312 ++++++++++++++++++------------------ 1 file changed, 159 insertions(+), 153 deletions(-) diff --git a/tests/test_ep_upload_e2e.py b/tests/test_ep_upload_e2e.py index a1521a96..56de5fea 100644 --- a/tests/test_ep_upload_e2e.py +++ b/tests/test_ep_upload_e2e.py @@ -61,24 +61,6 @@ def mock_env_variables(monkeypatch): monkeypatch.setattr("eval_protocol.evaluation.get_fireworks_account_id", lambda: "test_account") -@pytest.fixture -def mock_requests_get(): - """Mock requests.get for force flow check""" - with patch("requests.get") as mock_get: - mock_get.return_value.status_code = 404 # Evaluator doesn't exist - mock_get.return_value.raise_for_status = MagicMock() - yield mock_get - - -@pytest.fixture -def mock_requests_delete(): - """Mock requests.delete for force flow""" - with patch("requests.delete") as mock_delete: - mock_delete.return_value.status_code = 200 - mock_delete.return_value.raise_for_status = MagicMock() - yield mock_delete - - @pytest.fixture def mock_gcs_upload(): """Mock the GCS upload via requests.Session""" @@ -96,45 +78,77 @@ def mock_gcs_upload(): @pytest.fixture -def mock_requests_post(): - """Mock requests.post for all API endpoints""" - with patch("requests.post") as mock_post: - validate_response = {"success": True, "valid": True} - create_response = { - "name": "accounts/test_account/evaluators/test-eval", - "displayName": "Test Evaluator", - "description": "Test description", - } - - def side_effect(*args, **kwargs): - url = args[0] - payload = kwargs.get("json", {}) - response = mock_post.return_value - - if "getUploadEndpoint" in url: - # Dynamically create signed URLs for whatever filenames are requested - filename_to_size = payload.get("filename_to_size", {}) - signed_urls = {} - for filename in filename_to_size.keys(): - signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" - response.json.return_value = {"filenameToSignedUrls": signed_urls} - elif "validateUpload" in url: - response.json.return_value = validate_response - else: - # Create evaluator endpoint - response.json.return_value = create_response - - response.status_code = 200 +def mock_fireworks_client(): + """Mock the Fireworks SDK client used in evaluation.py""" + with patch("eval_protocol.evaluation.Fireworks") as mock_fw_class: + mock_client = MagicMock() + mock_fw_class.return_value = mock_client + + # Mock evaluators.create response + mock_create_response = MagicMock() + mock_create_response.name = "accounts/test_account/evaluators/test-eval" + mock_create_response.display_name = "Test Evaluator" + mock_create_response.description = "Test description" + mock_client.evaluators.create.return_value = mock_create_response + + # Mock evaluators.get_upload_endpoint response - will be set dynamically + def get_upload_endpoint_side_effect(evaluator_id, filename_to_size): + response = MagicMock() + signed_urls = {} + for filename in filename_to_size.keys(): + signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" + response.filename_to_signed_urls = signed_urls return response - mock_post.side_effect = side_effect - mock_post.return_value.status_code = 200 - mock_post.return_value.raise_for_status = MagicMock() - yield mock_post + mock_client.evaluators.get_upload_endpoint.side_effect = get_upload_endpoint_side_effect + + # Mock evaluators.validate_upload response + mock_validate_response = MagicMock() + mock_validate_response.success = True + mock_validate_response.valid = True + mock_client.evaluators.validate_upload.return_value = mock_validate_response + + # Mock evaluators.get (for force flow - raises NotFoundError by default) + import fireworks + + mock_client.evaluators.get.side_effect = fireworks.NotFoundError( + "Evaluator not found", + response=MagicMock(status_code=404), + body={"error": "not found"}, + ) + + # Mock evaluators.delete + mock_client.evaluators.delete.return_value = None + + yield mock_client + + +@pytest.fixture +def mock_platform_api_client(): + """Mock the Fireworks SDK client used in platform_api.py for secrets""" + with patch("eval_protocol.platform_api.Fireworks") as mock_fw_class: + mock_client = MagicMock() + mock_fw_class.return_value = mock_client + + # Mock secrets.get - raise NotFoundError to simulate secret doesn't exist + from fireworks import NotFoundError + + mock_client.secrets.get.side_effect = NotFoundError( + "Secret not found", + response=MagicMock(status_code=404), + body={"error": "not found"}, + ) + + # Mock secrets.create - successful + mock_create_response = MagicMock() + mock_create_response.name = "accounts/test_account/secrets/test-secret" + mock_client.secrets.create.return_value = mock_create_response + + yield mock_client def test_ep_upload_discovers_and_uploads_evaluation_test( - mock_env_variables, mock_requests_post, mock_requests_get, mock_gcs_upload, monkeypatch + mock_env_variables, mock_fireworks_client, mock_platform_api_client, mock_gcs_upload, monkeypatch ): """ Test the complete ep upload flow: @@ -213,20 +227,17 @@ async def test_simple_evaluation(row: EvaluationRow) -> EvaluationRow: # 4. VERIFY SUCCESS assert exit_code == 0, "Upload command should return 0 (success)" - # 5. VERIFY ALL API CALLS IN UPLOAD FLOW - post_calls = [call[0][0] for call in mock_requests_post.call_args_list] - - # Step 1: Create evaluator (V2 endpoint) - create_calls = [url for url in post_calls if "evaluatorsV2" in url] - assert len(create_calls) >= 1, "Should call V2 create endpoint" + # 5. VERIFY ALL API CALLS IN UPLOAD FLOW via Fireworks SDK + # Step 1: Create evaluator + assert mock_fireworks_client.evaluators.create.called, "Should call evaluators.create" # Step 2: Get upload endpoint - upload_endpoint_calls = [url for url in post_calls if "getUploadEndpoint" in url] - assert len(upload_endpoint_calls) >= 1, "Should call getUploadEndpoint" + assert mock_fireworks_client.evaluators.get_upload_endpoint.called, ( + "Should call evaluators.get_upload_endpoint" + ) # Step 3: Validate upload - validate_calls = [url for url in post_calls if "validateUpload" in url] - assert len(validate_calls) >= 1, "Should call validateUpload" + assert mock_fireworks_client.evaluators.validate_upload.called, "Should call evaluators.validate_upload" # Step 4: GCS upload assert mock_gcs_upload.send.called, "Should upload tar.gz to GCS" @@ -235,32 +246,22 @@ async def test_simple_evaluation(row: EvaluationRow) -> EvaluationRow: assert "storage.googleapis.com" in gcs_request.url, "Should upload to GCS" # 6. VERIFY CREATE PAYLOAD STRUCTURE - create_payload = None - for call in mock_requests_post.call_args_list: - url = call[0][0] - if "evaluatorsV2" in url: - create_payload = call[1].get("json") - break + create_call = mock_fireworks_client.evaluators.create.call_args + assert create_call is not None - assert create_payload is not None - assert "evaluator" in create_payload - assert create_payload["evaluatorId"] == "test-simple-eval" + # Check evaluator_id + assert create_call.kwargs.get("evaluator_id") == "test-simple-eval" - evaluator_data = create_payload["evaluator"] - assert evaluator_data["displayName"] == "Simple Word Count Eval" - assert evaluator_data["description"] == "E2E test evaluator" + # Check evaluator params + evaluator_params = create_call.kwargs.get("evaluator", {}) + assert evaluator_params.get("display_name") == "Simple Word Count Eval" + assert evaluator_params.get("description") == "E2E test evaluator" # Verify entry point is included - assert "entryPoint" in evaluator_data, "Should include entry point" - entry_point = evaluator_data["entryPoint"] + assert "entry_point" in evaluator_params, "Should include entry point" + entry_point = evaluator_params["entry_point"] assert "test_simple_eval.py::test_simple_evaluation" in entry_point - # Verify criteria structure (minimal, no embedded code) - criteria = evaluator_data["criteria"] - assert len(criteria) > 0 - assert criteria[0]["type"] == "CODE_SNIPPETS" - # Code is uploaded as tar.gz, not embedded in criteria - finally: # Restore original directory os.chdir(original_cwd) @@ -273,8 +274,8 @@ async def test_simple_evaluation(row: EvaluationRow) -> EvaluationRow: def test_ep_upload_with_parametrized_test( mock_env_variables, - mock_requests_post, - mock_requests_get, + mock_fireworks_client, + mock_platform_api_client, mock_gcs_upload, ): """ @@ -335,11 +336,10 @@ async def test_multi_model_eval(row: EvaluationRow) -> EvaluationRow: assert exit_code == 0 - # Verify upload flow completed - post_calls = [call[0][0] for call in mock_requests_post.call_args_list] - assert any("evaluatorsV2" in url for url in post_calls) - assert any("getUploadEndpoint" in url for url in post_calls) - assert any("validateUpload" in url for url in post_calls) + # Verify upload flow completed via Fireworks SDK + assert mock_fireworks_client.evaluators.create.called + assert mock_fireworks_client.evaluators.get_upload_endpoint.called + assert mock_fireworks_client.evaluators.validate_upload.called assert mock_gcs_upload.send.called finally: @@ -440,8 +440,8 @@ async def test_quickstart_eval(row: EvaluationRow) -> EvaluationRow: def test_ep_upload_complete_workflow_with_entry_point_validation( mock_env_variables, - mock_requests_post, - mock_requests_get, + mock_fireworks_client, + mock_platform_api_client, mock_gcs_upload, ): """ @@ -515,14 +515,12 @@ async def test_math_correctness(row: EvaluationRow) -> EvaluationRow: assert exit_code == 0 - # 3. VERIFY 5-STEP UPLOAD FLOW - post_calls = [call[0][0] for call in mock_requests_post.call_args_list] - + # 3. VERIFY 5-STEP UPLOAD FLOW via Fireworks SDK # Step 1: Create evaluator - assert any("evaluatorsV2" in url for url in post_calls), "Missing create call" + assert mock_fireworks_client.evaluators.create.called, "Missing create call" # Step 2: Get upload endpoint - assert any("getUploadEndpoint" in url for url in post_calls), "Missing getUploadEndpoint call" + assert mock_fireworks_client.evaluators.get_upload_endpoint.called, "Missing getUploadEndpoint call" # Step 3: Upload to GCS assert mock_gcs_upload.send.called, "Missing GCS upload" @@ -531,51 +529,34 @@ async def test_math_correctness(row: EvaluationRow) -> EvaluationRow: assert "storage.googleapis.com" in gcs_request.url # Step 4: Validate - assert any("validateUpload" in url for url in post_calls), "Missing validateUpload call" + assert mock_fireworks_client.evaluators.validate_upload.called, "Missing validateUpload call" # 4. VERIFY PAYLOAD DETAILS - create_payload = None - for call in mock_requests_post.call_args_list: - url = call[0][0] - if "evaluatorsV2" in url: - create_payload = call[1].get("json") - break - - assert create_payload is not None + create_call = mock_fireworks_client.evaluators.create.call_args + assert create_call is not None # Verify evaluator ID auto-generated from filename + test name - evaluator_id = create_payload["evaluatorId"] + evaluator_id = create_call.kwargs.get("evaluator_id", "") assert "test-math-eval" in evaluator_id or "math-correctness" in evaluator_id # Verify entry point is path-based (not module-based) - evaluator_data = create_payload["evaluator"] - assert "entryPoint" in evaluator_data, "Should include entry point" - entry_point = evaluator_data["entryPoint"] + evaluator_params = create_call.kwargs.get("evaluator", {}) + assert "entry_point" in evaluator_params, "Should include entry point" + entry_point = evaluator_params["entry_point"] assert "test_math_eval.py::test_math_correctness" in entry_point - # Verify criteria is minimal - criteria = evaluator_data["criteria"] - assert len(criteria) > 0 - assert criteria[0]["type"] == "CODE_SNIPPETS" - # Code is in tar.gz, not in payload - # 5. VERIFY TAR.GZ WAS CREATED AND UPLOADED # Check getUploadEndpoint call payload - upload_endpoint_payload = None - for call in mock_requests_post.call_args_list: - url = call[0][0] - if "getUploadEndpoint" in url: - upload_endpoint_payload = call[1].get("json") - break - - assert upload_endpoint_payload is not None - assert "filename_to_size" in upload_endpoint_payload + upload_call = mock_fireworks_client.evaluators.get_upload_endpoint.call_args + assert upload_call is not None + filename_to_size = upload_call.kwargs.get("filename_to_size", {}) + assert filename_to_size, "Should have filename_to_size" # Tar filename is dynamic (based on directory name) - tar_files = list(upload_endpoint_payload["filename_to_size"].keys()) + tar_files = list(filename_to_size.keys()) assert len(tar_files) == 1, "Should have exactly one tar file" tar_filename = tar_files[0] assert tar_filename.endswith(".tar.gz"), "Should be a tar.gz file" - tar_size = upload_endpoint_payload["filename_to_size"][tar_filename] + tar_size = int(filename_to_size[tar_filename]) assert tar_size > 0, "Tar file should have non-zero size" finally: @@ -587,8 +568,8 @@ async def test_math_correctness(row: EvaluationRow) -> EvaluationRow: def test_ep_upload_force_flag_triggers_delete_flow( mock_env_variables, - mock_requests_post, mock_gcs_upload, + mock_platform_api_client, ): """ Test that --force flag triggers the check/delete/recreate flow @@ -611,39 +592,64 @@ async def test_force_eval(row: EvaluationRow) -> EvaluationRow: try: os.chdir(test_project_dir) - # Mock requests.get to return 200 (evaluator exists) - with patch("requests.get") as mock_get: - mock_get.return_value.status_code = 200 - mock_get.return_value.raise_for_status = MagicMock() + # Mock the Fireworks client with evaluator existing (for force flow) + with patch("eval_protocol.evaluation.Fireworks") as mock_fw_class: + mock_client = MagicMock() + mock_fw_class.return_value = mock_client + + # Mock evaluators.get to return an existing evaluator (not raise NotFoundError) + mock_existing_evaluator = MagicMock() + mock_existing_evaluator.name = "accounts/test_account/evaluators/test-force" + mock_client.evaluators.get.return_value = mock_existing_evaluator + + # Mock evaluators.delete + mock_client.evaluators.delete.return_value = None + + # Mock evaluators.create response + mock_create_response = MagicMock() + mock_create_response.name = "accounts/test_account/evaluators/test-force" + mock_client.evaluators.create.return_value = mock_create_response + + # Mock get_upload_endpoint + def get_upload_endpoint_side_effect(evaluator_id, filename_to_size): + response = MagicMock() + signed_urls = {} + for filename in filename_to_size.keys(): + signed_urls[filename] = f"https://storage.googleapis.com/test-bucket/{filename}?signed=true" + response.filename_to_signed_urls = signed_urls + return response + + mock_client.evaluators.get_upload_endpoint.side_effect = get_upload_endpoint_side_effect + + # Mock validate_upload + mock_client.evaluators.validate_upload.return_value = MagicMock() - # Mock requests.delete - with patch("requests.delete") as mock_delete: - mock_delete.return_value.status_code = 200 - mock_delete.return_value.raise_for_status = MagicMock() + discovered_tests = _discover_tests(test_project_dir) - discovered_tests = _discover_tests(test_project_dir) + args = argparse.Namespace( + path=test_project_dir, + entry=None, + id="test-force", + display_name=None, + description=None, + force=True, # Force flag enabled + yes=True, + ) - args = argparse.Namespace( - path=test_project_dir, - entry=None, - id="test-force", - display_name=None, - description=None, - force=True, # Force flag enabled - yes=True, - ) + with patch("eval_protocol.cli_commands.upload._prompt_select") as mock_select: + mock_select.return_value = discovered_tests + exit_code = upload_command(args) - with patch("eval_protocol.cli_commands.upload._prompt_select") as mock_select: - mock_select.return_value = discovered_tests - exit_code = upload_command(args) + assert exit_code == 0 - assert exit_code == 0 + # Verify check happened (evaluators.get was called) + assert mock_client.evaluators.get.called, "Should check if evaluator exists" - # Verify check happened - assert mock_get.called, "Should check if evaluator exists" + # Verify delete happened (since evaluator existed) + assert mock_client.evaluators.delete.called, "Should delete existing evaluator" - # Verify delete happened (since mock_get returned 200) - assert mock_delete.called, "Should delete existing evaluator" + # Verify create happened after delete + assert mock_client.evaluators.create.called, "Should create evaluator after delete" finally: os.chdir(original_cwd) From 3d9c80d4e0dc6f4907eebae2a7bfd552e9d02403 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 30 Dec 2025 09:16:04 -0800 Subject: [PATCH 15/16] Add secret selection prompt for Fireworks upload in CLI - Introduced `_prompt_select_secrets` function to allow users to select environment variables for upload as secrets. - Implemented fallback selection method for non-interactive environments. - Updated `upload_command` to utilize the new secret selection logic. - Enhanced user experience with improved prompts and error handling. - Added `_get_questionary_style` function for consistent CLI styling. --- eval_protocol/cli_commands/upload.py | 149 +++++++++++++++++++++++---- eval_protocol/cli_commands/utils.py | 69 ++++++++----- 2 files changed, 173 insertions(+), 45 deletions(-) diff --git a/eval_protocol/cli_commands/upload.py b/eval_protocol/cli_commands/upload.py index 9e079c3e..b162fb0e 100644 --- a/eval_protocol/cli_commands/upload.py +++ b/eval_protocol/cli_commands/upload.py @@ -1,4 +1,5 @@ import argparse +from eval_protocol.cli_commands.utils import DiscoveredTest import importlib.util import os import re @@ -14,10 +15,9 @@ _build_entry_point, _build_evaluator_dashboard_url, _discover_and_select_tests, - _discover_tests, _ensure_account_id, + _get_questionary_style, _normalize_evaluator_id, - _prompt_select, ) @@ -170,6 +170,109 @@ def _mask_secret_value(value: str) -> str: return "" +def _prompt_select_secrets( + secrets: Dict[str, str], + secrets_from_env_file: Dict[str, str], + non_interactive: bool, +) -> Dict[str, str]: + """ + Prompt user to select which environment variables to upload as secrets. + Returns the selected secrets. + """ + if not secrets: + return {} + + if non_interactive: + return secrets + + # Check if running in a non-TTY environment (e.g., CI/CD) + if not sys.stdin.isatty(): + return secrets + + try: + import questionary + + custom_style = _get_questionary_style() + + # Build choices with source info and masked values + choices = [] + for key, value in secrets.items(): + source = ".env" if key in secrets_from_env_file else "env" + masked = _mask_secret_value(value) + label = f"{key} ({source}: {masked})" + choices.append(questionary.Choice(title=label, value=key, checked=True)) + + if len(choices) == 0: + return {} + + print("\nFound environment variables to upload as Fireworks secrets:") + selected_keys = questionary.checkbox( + "Select secrets to upload:", + choices=choices, + style=custom_style, + pointer=">", + instruction="(↑↓ move, space select, enter confirm)", + ).ask() + + if selected_keys is None: + # User cancelled with Ctrl+C + print("\nSecret upload cancelled.") + return {} + + return {k: v for k, v in secrets.items() if k in selected_keys} + + except ImportError: + # Fallback to simple text-based selection + return _prompt_select_secrets_fallback(secrets, secrets_from_env_file) + except KeyboardInterrupt: + print("\n\nSecret upload cancelled.") + return {} + + +def _prompt_select_secrets_fallback( + secrets: Dict[str, str], + secrets_from_env_file: Dict[str, str], +) -> Dict[str, str]: + """Fallback prompt selection for when questionary is not available.""" + print("\n" + "=" * 60) + print("Found environment variables to upload as Fireworks secrets:") + print("=" * 60) + print("\nTip: Install questionary for better UX: pip install questionary\n") + + secret_list = list(secrets.items()) + for idx, (key, value) in enumerate(secret_list, 1): + source = ".env" if key in secrets_from_env_file else "env" + masked = _mask_secret_value(value) + print(f" [{idx}] {key} ({source}: {masked})") + + print("\n" + "=" * 60) + print("Enter numbers to select (comma-separated), 'all' for all, or 'none' to skip:") + + try: + choice = input("Selection: ").strip().lower() + except KeyboardInterrupt: + print("\nSecret upload cancelled.") + return {} + + if not choice or choice == "none": + return {} + + if choice == "all": + return secrets + + try: + indices = [int(x.strip()) for x in choice.split(",")] + selected = {} + for idx in indices: + if 1 <= idx <= len(secret_list): + key, value = secret_list[idx - 1] + selected[key] = value + return selected + except ValueError: + print("Invalid input. Skipping secret upload.") + return {} + + def upload_command(args: argparse.Namespace) -> int: root = os.path.abspath(getattr(args, "path", ".")) entries_arg = getattr(args, "entry", None) @@ -181,7 +284,7 @@ def upload_command(args: argparse.Namespace) -> int: qualname, resolved_path = _resolve_entry_to_qual_and_source(e, root) selected_specs.append((qualname, resolved_path)) else: - selected_tests = _discover_and_select_tests(root, non_interactive=non_interactive) + selected_tests: list[DiscoveredTest] | None = _discover_and_select_tests(root, non_interactive=non_interactive) if not selected_tests: return 1 selected_specs = [(t.qualname, t.file_path) for t in selected_tests] @@ -212,24 +315,34 @@ def upload_command(args: argparse.Namespace) -> int: secrets_from_file["FIREWORKS_API_KEY"] = fw_api_key_value if fw_account_id and secrets_from_file: - print(f"Found {len(secrets_from_file)} API keys to upload as Fireworks secrets...") if secrets_from_env_file and os.path.exists(env_file_path): print(f"Loading secrets from: {env_file_path}") - for secret_name, secret_value in secrets_from_file.items(): - source = ".env" if secret_name in secrets_from_env_file else "environment" - print( - f"Ensuring {secret_name} is registered as a secret on Fireworks for rollout... " - f"({source}: {_mask_secret_value(secret_value)})" - ) - if create_or_update_fireworks_secret( - account_id=fw_account_id, - key_name=secret_name, - secret_value=secret_value, - ): - print(f"✓ {secret_name} secret created/updated on Fireworks.") - else: - print(f"Warning: Failed to create/update {secret_name} secret on Fireworks.") + # Prompt user to select which secrets to upload + selected_secrets = _prompt_select_secrets( + secrets_from_file, + secrets_from_env_file, + non_interactive, + ) + + if selected_secrets: + print(f"\nUploading {len(selected_secrets)} selected secret(s) to Fireworks...") + for secret_name, secret_value in selected_secrets.items(): + source = ".env" if secret_name in secrets_from_env_file else "environment" + print( + f"Ensuring {secret_name} is registered as a secret on Fireworks for rollout... " + f"({source}: {_mask_secret_value(secret_value)})" + ) + if create_or_update_fireworks_secret( + account_id=fw_account_id, + key_name=secret_name, + secret_value=secret_value, + ): + print(f"✓ {secret_name} secret created/updated on Fireworks.") + else: + print(f"Warning: Failed to create/update {secret_name} secret on Fireworks.") + else: + print("No secrets selected for upload.") else: if not fw_account_id: print( diff --git a/eval_protocol/cli_commands/utils.py b/eval_protocol/cli_commands/utils.py index 3ea09a8a..3f941d4b 100644 --- a/eval_protocol/cli_commands/utils.py +++ b/eval_protocol/cli_commands/utils.py @@ -23,6 +23,28 @@ from ..fireworks_rft import _map_api_host_to_app_host +def _get_questionary_style(): + """Get the shared questionary style for CLI prompts - minimal and clean.""" + try: + from questionary import Style + + return Style( + [ + ("qmark", "fg:#888888"), + ("question", "bold"), + ("answer", "noinherit"), + ("pointer", "noinherit"), + ("highlighted", "noinherit"), + ("selected", "noinherit"), + ("separator", "noinherit"), + ("instruction", "noinherit fg:#888888"), + ("text", "noinherit"), + ] + ) + except ImportError: + return None + + @dataclass class DiscoveredTest: module_path: str @@ -233,22 +255,8 @@ def _prompt_select_interactive(tests: list[DiscoveredTest]) -> list[DiscoveredTe """Interactive selection with arrow keys using questionary.""" try: import questionary - from questionary import Style - # Custom style similar to Vercel CLI - custom_style = Style( - [ - ("qmark", "fg:#673ab7 bold"), - ("question", "bold"), - ("answer", "fg:#2196f3 bold"), - ("pointer", "fg:#673ab7 bold"), - ("highlighted", "fg:#673ab7 bold"), - ("selected", "fg:#cc5454"), - ("separator", "fg:#cc5454"), - ("instruction", ""), - ("text", ""), - ] - ) + custom_style = _get_questionary_style() # Check if only one test - auto-select it if len(tests) == 1: @@ -259,25 +267,31 @@ def _prompt_select_interactive(tests: list[DiscoveredTest]) -> list[DiscoveredTe else: return [] - # Single-select UX - print("\n") - print("Tip: Use ↑/↓ arrows to navigate and press ENTER to select.\n") - + # Build checkbox choices choices = [] for idx, t in enumerate(tests, 1): choice_text = _format_test_choice(t, idx) - choices.append({"name": choice_text, "value": idx - 1}) + choices.append(questionary.Choice(title=choice_text, value=idx - 1, checked=False)) - selected = questionary.select( - "Select an evaluation test to upload:", choices=choices, style=custom_style + print() + selected_indices = questionary.checkbox( + "Select evaluation tests to upload:", + choices=choices, + style=custom_style, + pointer=">", + instruction="(↑↓ move, space select, enter confirm)", ).ask() - if selected is None: # Ctrl+C + if selected_indices is None: # Ctrl+C print("\nUpload cancelled.") return [] - print("\n✓ Selected 1 test") - return [tests[selected]] + if not selected_indices: + return [] + + selected_tests = [tests[i] for i in selected_indices] + print(f"\n✓ Selected {len(selected_tests)} test(s)") + return selected_tests except ImportError: # Fallback to simpler implementation @@ -355,8 +369,9 @@ def _discover_and_select_tests(project_root: str, non_interactive: bool) -> Opti try: selected_tests = _prompt_select(tests, non_interactive=non_interactive) - except Exception: - print("Error: Failed to open selector UI. Please pass --evaluator or --entry explicitly.") + except Exception as e: + print(f"Error: Failed to open selector UI: {e}") + print("Please pass --evaluator or --entry explicitly.") return None if not selected_tests: From a785e660d0c7b15a7398608a539dd1cd1d100e70 Mon Sep 17 00:00:00 2001 From: Dylan Huang Date: Tue, 30 Dec 2025 11:02:14 -0800 Subject: [PATCH 16/16] try again --- eval_protocol/cli_commands/upload.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eval_protocol/cli_commands/upload.py b/eval_protocol/cli_commands/upload.py index b162fb0e..8a25b49b 100644 --- a/eval_protocol/cli_commands/upload.py +++ b/eval_protocol/cli_commands/upload.py @@ -15,9 +15,11 @@ _build_entry_point, _build_evaluator_dashboard_url, _discover_and_select_tests, + _discover_tests, _ensure_account_id, _get_questionary_style, _normalize_evaluator_id, + _prompt_select, )